前言
提示:这里可以添加本文要记录的大概内容:
引自paddle飞浆社区的开源项目使用Paddle实现MRI医学图像超分辨率项目
本文为学习MRI超分辨率使用深度学习实现的各部分代码,作记录用
提示:以下是本篇文章正文内容,下面案例可供参考
一、数据获取及环境配置
1、压缩包解压
# 1. 由于压缩包分包了,所以我们需要将它放在一个压缩包内,随后再解压 -q 执行时不显示任何信息
!zip -q -s 0 ./data/data155555/IXISR.zip --out ./data/data155555/IXI.zip
#将分卷文件合并成一个单独的文件
# 2. 随后解压IXISR数据集
!unzip -q -o ./data/data155555/IXI.zip -d ./data/data155555/
#unzip解压,-o:解压时覆盖已经存在的文件,并且无需用户确认
!echo "unzip finish!!"
#echo 通常用于 shell 脚本中,用于显示消息或输出其他命令的结果。
2、超参数设置
“”"
#scale
#epoch
#data_dir
“”"
# 3. 设置一下各种文件地址
#可更改,每种数据对应的训练模型使用的样本
data_mode_index = 0
data_type_index = 2
#采样模式
data_mode = ['bicubic_2x','bicubic_3x','bicubic_4x','truncation_2x','truncation_3x','truncation_4x']
#数据类型
data_type = ['PD','T1','T2']
SCALE = 2
#训练轮数
EPOCH = 20
# ./data/data155555/IXI/IXI_LR/bicubic_2x/PD/train
#格式化生成存放各数据的相对路径
train_LR_path = './data/data155555/IXI/IXI_LR/{}/{}/train'.format(data_mode[data_mode_index],data_type[data_type_index])
train_HR_path = './data/data155555/IXI/IXI_HR/{}/train'.format(data_type[data_type_index])
valid_LR_path = './data/data155555/IXI/IXI_LR/{}/{}/valid'.format(data_mode[data_mode_index],data_type[data_type_index])
valid_HR_path = './data/data155555/IXI/IXI_HR/{}/valid'.format(data_type[data_type_index])
test_LR_path = './data/data155555/IXI/IXI_LR/{}/{}/test'.format(data_mode[data_mode_index],data_type[data_type_index])
test_HR_path = './data/data155555/IXI/IXI_HR/{}/test'.format(data_type[data_type_index])
3、加载数据集
from glob import glob
import os
import numpy as np
#1、os.path.join()路径合成函数
#2、sorted()排序路径
#3、glob模块用来查找文件目录和文件,并将搜索的到的结果返回到一个列表中,使用通用匹配符,
#可同时获得所有符合的文件路径
train_LR_paths = sorted(glob(os.path.join(train_LR_path,"*_*.npy")))
#将数据按文件名遍历取出
train_LR_data = np.array([np.load(fname) for fname in train_LR_paths], dtype='float32')
train_HR_paths = sorted(glob(os.path.join(train_HR_path,"*_*.npy")))
# print(train_HR_paths)
train_HR_data = np.array([np.load(fname) for fname in train_HR_paths], dtype='float32')
valid_LR_paths = sorted(glob(os.path.join(valid_LR_path,"*_*.npy")))
valid_LR_data = np.array([np.load(fname) for fname in valid_LR_paths], dtype='float32')
valid_HR_paths = sorted(glob(os.path.join(valid_HR_path,"*_*.npy")))
valid_HR_data = np.array([np.load(fname) for fname in valid_HR_paths], dtype='float32')
test_LR_paths = sorted(glob(os.path.join(test_LR_path,"*_*.npy")))
test_LR_data = np.array([np.load(fname) for fname in test_LR_paths], dtype='float32')
test_HR_paths = sorted(glob(os.path.join(test_HR_path,"*_*.npy")))
test_HR_data = np.array([np.load(fname) for fname in test_HR_paths], dtype='float32')
# 查看数据维度为多少
print(train_LR_data.shape)
print(train_HR_data.shape)
print(valid_LR_data.shape)
print(valid_HR_data.shape)
print(test_LR_data.shape)
print(test_HR_data.shape)
二、自定义数据加载类
1、数据预处理
“”"
#3D数据转化2D
“”"
#将3D切片数据集转化为2D切片数据方便训练
#transpose()函数的作用就是调换数组的行列值的索引值
#B,H,W,C变成B,C,H,W
train_LR_data = train_LR_data.transpose([0,3,1,2])
train_HR_data = train_HR_data.transpose([0,3,1,2])
valid_LR_data = valid_LR_data.transpose([0,3,1,2])
valid_HR_data = valid_HR_data.transpose([0,3,1,2])
test_LR_data = test_LR_data .transpose([0,3,1,2])
test_HR_data = test_HR_data .transpose([0,3,1,2])
#利用reshape函数调整维度
#保留宽/高不变
train_LR_data = train_LR_data.reshape(-1,train_LR_data.shape[2],train_LR_data.shape[3])
train_HR_data = train_HR_data.reshape(-1,train_HR_data.shape[2],train_HR_data.shape[3])
valid_LR_data = valid_LR_data.reshape(-1,valid_LR_data.shape[2],valid_LR_data.shape[3])
valid_HR_data = valid_HR_data.reshape(-1,valid_HR_data.shape[2],valid_HR_data.shape[3])
test_LR_data = test_LR_data .reshape(-1,test_LR_data .shape[2],test_LR_data .shape[3])
test_HR_data = test_HR_data .reshape(-1,test_HR_data .shape[2],test_HR_data .shape[3])
print(train_LR_data.shape)
print(train_HR_data.shape)
print(valid_LR_data.shape)
print(valid_HR_data.shape)
print(test_LR_data.shape)
print(test_HR_data.shape)
2、自定义Dataset类
# 没有进行patch分割成小块训练的
from paddle.io import Dataset # 导入类库 Dataset
from paddle.vision.transforms import ToTensor
class MyDataset(Dataset): # 定义Dataset的子类MyDataset
#初始化定义数据模式和数据transform方法
def __init__(self, mode='train',transform=ToTensor()):
super(MyDataset, self).__init__()
self.mode = mode
#根据mode选择对应数据
if mode == 'train':
self.LR_data = train_LR_data
self.HR_data = train_HR_data
elif mode == 'valid':
self.LR_data = valid_LR_data
self.HR_data = valid_HR_data
else:
self.LR_data = test_LR_data
self.HR_data = test_HR_data
self.transform = transform
def data_augmentation(self,LR, HR): # 数据增强:随机翻转、旋转
#生成随机数
if np.random.randint(2) == 1:
#三维切片[:,:,:-1],从后向前全选
LR = LR[:,::-1]
HR = HR[:,::-1]
n = np.random.randint(4)
if n == 1:
#调换维度实现旋转
LR = LR[::-1,:].transpose([1,0])
HR = HR[::-1,:].transpose([1,0])
if n == 2:
LR = LR[::-1,::-1]
HR = HR[::-1,::-1]
if n == 3:
LR = LR[:,::-1].transpose([1,0])
HR = HR[:,::-1].transpose([1,0])
return LR, HR
def __getitem__(self, index):
LR = self.LR_data[index]
HR = self.HR_data[index]
# 最大最小归一化
LR = (LR-LR.min())/(LR.max()-LR.min())
HR = (HR-HR.min())/(HR.max()-HR.min())
# Z-score归一化
# LR = (LR - LR_means) / LR_stdevs
# HR = (HR - HR_means) / HR_stdevs
# 修改归一化
if(self.mode=='train'):
LR,HR = self.data_augmentation(LR,HR)
LR = self.transform(LR)
HR = self.transform(HR)
return LR, HR
def __len__(self):
return self.LR_data.shape[0]
三、自定义评估函数Metrics
1、评估函数定义及作用
有各种各样的指标,我们可以评估ML算法的性能。
Metrics是一个开源的原生的函数和度量模块的集合,用于简单的性能评估。你可以使用开箱即用的实现来实现常见的指标,如准确性,召回率,精度,AUROC, RMSE, R²等,或者创建自己的指标。
2、MRI超分辨率任务评估指标类型
- PSNR
- SSIM
- MSE
- RMSE
- NRMSE
以上指标均为不被支持的指标,需要调用API创建自定义指标
3、实现自己的metrics
- init():初始化
示例
def __init__(self, name='psnr', *args, **kwargs):
super(PSNR, self).__init__(*args, **kwargs)
self.psnr = 0
self._name = name
self.count = 0
指标名称及初始化、计算次数初始化
- update(self, preds, labels):计算过程的迭代
示例
def update(self, preds, labels):
#判断preds是否为tensor类型,网络计算过程需为tensor
if isinstance(preds, paddle.Tensor):
#isinstance()函数用来判别两变量类型是否一致
preds = preds.numpy()
#将tensor类型转换为ndarray
elif not self._is_numpy_(preds):
raise ValueError("The 'preds' must be a numpy ndarray or Tensor.")
if isinstance(labels, paddle.Tensor):
labels = labels.numpy()
elif not self._is_numpy_(labels):
raise ValueError("The 'labels' must be a numpy ndarray or Tensor.")
B,C,H,W = preds.shape[0],preds.shape[1],preds.shape[2],preds.shape[3]
#B:批量大小 C:通道 H:高度 W:宽度
#获取预测样本的维度,方便作循环参数
temp = 0
#分别用批样本数和通道数循环计算所有样本的评估指标
for b in range(B):
for c in range(C):
temp +=
skimage.metrics.peak_signal_noise_ratio(preds[b,c,:,:], labels[b,c,:,:], data_range=1)
#计算对应的preds和labels的PSNR
#样本平均化
temp = temp/(B*C)
#评估指标和计算次数更新
self.psnr += temp
self.count += 1
- reset(): 每个Epoch结束后进行评估指标的重置,这样下个Epoch可以重新进行计算
示例
def reset(self):
##该epoch计算完成并返回后清零
self.psnr = 0
self.count = 0
- accumulate():计算最终的指标
- 示例
def accumulate(self):
return self.psnr/self.count
- name(self): 实现name方法,返回定义的评估指标名字
- 示例
def name(self):
return self._name
四、自定义损失函数
1、常用的损失函数类型
- SSIM
- 边缘Loss(AdvSoberLoss)
- 感知损失(Perceptual loss)
- L1_Charbonnier_loss
- nTVLoss
2、各类型损失函数详解
①生成用来卷积的归一化的高斯核
# 计算一维的高斯分布向量
def gaussian(window_size, sigma):
#用各个窗口大小创建一维高斯函数
gauss = paddle.to_tensor([exp(-(x - window_size/2)**2/float(2*sigma**2)) for x in range(window_size)])
return gauss/gauss.sum()
# 创建高斯核,通过两个一维高斯分布向量进行矩阵乘法得到
def create_window(window_size, channel):
_1D_window = gaussian(window_size, 1.5).unsqueeze(1)
#unsqueeze()函数起升维的作用,1在列方向扩维。
_2D_window = _1D_window.mm(_1D_window.t()).astype('float32').unsqueeze(0).unsqueeze(0)
#先相乘后升维
#a.mm(b)=a*b
#astpye就是转换numpy数组的数据类型,转换到float32
window = paddle.to_tensor(_2D_window.expand([channel, 1, window_size, window_size]))
#维度拓展,暂不知其为何
return window
②SSIM_Loss
# 计算SSIM
# 直接使用SSIM的公式,但是在计算均值时,不是直接求像素平均值,而是采用归一化的高斯核卷积来代替。
# 在计算方差和协方差时用到了公式Var(X)=E[X^2]-E[X]^2, cov(X,Y)=E[XY]-E[X]E[Y].
# 正如前面提到的,上面求期望的操作采用高斯核卷积代替
def _ssim(img1, img2, window, window_size, channel, size_average = True):
#转为tensor类型
img1= paddle.to_tensor(img1)
img2= paddle.to_tensor(img2)
# 高斯滤波 求期望E[X]
mu1 = F.conv2d(img1, window, padding = window_size//2, groups = channel)
mu2 = F.conv2d(img2, window, padding = window_size//2, groups = channel)
mu1_sq = mu1.pow(2)#平方
mu2_sq = mu2.pow(2)
mu1_mu2 = mu1*mu2#乘积
#用高斯核卷积求期望
#Var(X)=E[X^2]-E[X]^2
sigma1_sq = F.conv2d(img1*img1, window, padding = window_size//2, groups = channel) - mu1_sq
sigma2_sq = F.conv2d(img2*img2, window, padding = window_size//2, groups = channel) - mu2_sq
#cov(X,Y)=E[XY]-E[X]E[Y]
sigma12 = F.conv2d(img1*img2, window, padding = window_size//2, groups = channel) - mu1_mu2
C1 = 0.01**2
C2 = 0.03**2
#计算SSIM
ssim_map = ((2*mu1_mu2 + C1)*(2*sigma12 + C2))/((mu1_sq + mu2_sq + C1)*(sigma1_sq + sigma2_sq + C2))
#求平均
if size_average:
return ssim_map.mean()
else:
#求均值,1和-1对行
return ssim_map.mean(1).mean(1).mean(1)
#类重用窗口
class SSIMLoss(nn.Layer):
‘’‘
初始化
前向
’‘’
def __init__(self, window_size = 11, size_average = True):
#对继承自父类的属性进行初始化。而且是用父类的初始化方法来初始化继承的属性。
super(SSIMLoss, self).__init__()
self.window_size = window_size
self.size_average = size_average
self.channel = 1
self.window = create_window(window_size, self.channel)
#'mean':输出的总和除以输出中元素的数量,'sum':输出的总和
self.l1=paddle.nn.L1Loss(reduction='mean')
def forward(self, img1, img2):
l1loss = self.l1(img1,img2)
(_, channel, _, _) = img1.shape
#判断通道数是否为1,否则重新建立高斯窗并重新赋值
if channel == self.channel:
window = self.window
else:
window = create_window(self.window_size, channel)
self.window = window
self.channel = channel
return l1loss+0.2*(1 - _ssim(img1, img2, window, self.window_size, channel, self.size_average))
def ssim(img1, img2, window_size = 11, size_average = True):
(_, channel, _, _) = img1.shape
window = create_window(window_size, channel)
return _ssim(img1, img2, window, window_size, channel, size_average)
③感知损失函数
class LossVGG19(nn.Layer):
def __init__(self):
super(LossVGG19, self).__init__()
self.vgg_layers = vgg19(pretrained=True).features
self.layer_name_mapping = {
'3': "relu1",
'8': "relu2",
'13': "relu3",
'22': "relu4",
'31': "relu5", # 1_2 to 5_2
}
def forward(self, x):
output = {}
for name, module in self.vgg_layers._sub_layers.items():
x = module(x)
if name in self.layer_name_mapping:
output[self.layer_name_mapping[name]] = x
return output
def loss_textures(x, y, nc=3, alpha=1.2, margin=0):
xi = x.reshape([x.shape[0], -1, nc, x.shape[2], x.shape[3]])
yi = y.reshape([y.shape[0], -1, nc, y.shape[2], y.shape[3]])
xi2 = paddle.sum(xi * xi, axis=2)
yi2 = paddle.sum(yi * yi, axis=2)
out = paddle.nn.functional.relu(yi2 * alpha - xi2 + margin)
return paddle.mean(out)
五、自定义回调函数
1、回调函数作用
训练模型时自动存储每轮训练得到的模型
2、程序
示例
class ModelCheckpoint(paddle.callbacks.Callback):
"""
继承paddle.callbacks.Callback
"""
def __init__(self, save_freq=1, save_dir=None):
"""
构造函数实现
"""
self.save_freq = save_freq
#保存地址
self.save_dir = save_dir
#保存模型指标大小
self.max_psnr = 0
self.max_ssim = 0
def on_epoch_begin(self, epoch=None, logs=None):
"""
每轮训练开始前,获取当前轮数
"""
self.epoch = epoch
def _is_save(self):
return self.model and self.save_dir and ParallelEnv().local_rank == 0
#ParallelEnv():用于获取动态图模型并行执行所需的环境变量值
def on_epoch_end(self, epoch, logs=None):
"""
每轮训练结束后,保存验证集最大psnr和ssim的checkpoint
"""
#代入模型得到验证结果
eval_result = self.model.evaluate(valid_dataset, verbose=1)
#verbose=1,日志显示格式带进度条
#检查当前checkpoint的ssim和psnr是否更大
#如果更大则需记录当前指标及保存checkpoint的地址
if self._is_save() and eval_result['psnr'] >= self.max_psnr and eval_result['ssim'] >= self.max_ssim:
self.max_psnr = eval_result['psnr']
self.max_ssim = eval_result['ssim']
path = '{}/{}_{}'.format(self.save_dir, self.max_psnr, self.max_ssim)
print('save checkpoint at {}'.format(os.path.abspath(path)))
self.model.save(path)
# def on_train_end(self, logs=None):
# """
# 训练结束后,保存最后一轮的checkpoint
# """
# if self._is_save():
# path = '{}/final'.format(self.save_dir)
# print('save checkpoint at {}'.format(os.path.abspath(path)))
# self.model.save(path)
六、自定义模型
1、Sub_Pixel_CNN模型(2016)
①模型结构
这篇论文中,提出了一种在神经网络的末端增加分辨率的方法。这解决了一个问题,那就是大部分的超分辨率操作没有必要在较大的高分辨率图片上面执行。文章采用了高效的子像素卷积层去学习倍增分辨率的操作
②编程实现
四层卷积+末端像素清洗
class Sub_Pixel_CNN(nn.Layer):
# Sub_Pixel_CNN(upscale_factor=2, channels=1)
def __init__(self, upscale_factor=2, channels=1):
super(Sub_Pixel_CNN, self).__init__()
self.conv1 = paddle.nn.Conv2D(channels,64,5,stride=1, padding=2)
self.conv2 = paddle.nn.Conv2D(64,64,3,stride=1, padding=1)
self.conv3 = paddle.nn.Conv2D(64,32,3,stride=1, padding=1)
#最后一层卷积实现上采样
self.conv4 = paddle.nn.Conv2D(32,channels * (upscale_factor ** 2),3,stride=1, padding=1)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.conv4(x)
x = paddle.nn.functional.pixel_shuffle(x,2)
return x
2、VRCNN模型(2017)
①模型结构
作者参考GoogleNet的思想,增加网络深度的同时,也在网络宽度上进行扩展,即使用多个小尺寸的卷积窗的并行组合来替换单个大尺寸卷积核,不同尺寸的卷积核可以提取到不同层次的图像特征,因此使用这种方法,可以在一层中整合图像的多种特征,有利于图像的重建。
②代码实现
定义卷积序列(Conv+BN+ReLu),两次Concat连接,残差连接
class VRCNN(nn.Layer):
def __init__(self):
super(VRCNN, self).__init__()
self.conv1 = nn.Sequential(
# 定义一个卷积层conv1,输入通道是1,输出通道64,卷积核大小为5,步长为1,padding为2,使用relu激活函数
nn.Conv2D(1, 64, 5, stride=1, padding=2),
nn.BatchNorm(64),
nn.ReLU(),
)
self.conv2 = nn.Sequential(
# 定义一个卷积层conv2,输入通道是64,输出通道16,卷积核大小为5,步长为1,padding为2,使用relu激活函数
nn.Conv2D(64, 16, 5, stride=1, padding=2),
nn.BatchNorm(16),
nn.ReLU(),
)
self.conv3 = nn.Sequential(
# 定义一个卷积层conv3,输入通道是64,输出通道32,卷积核大小为3,步长为1,padding为1,使用relu激活函数
nn.Conv2D(64, 32, 3, stride=1, padding=1),
nn.BatchNorm(32),
nn.ReLU(),
)
self.conv4 = nn.Sequential(
# 定义一个卷积层conv4,输入通道是48,输出通道16,卷积核大小为3,步长为1,padding为1,使用relu激活函数
nn.Conv2D(48, 16, 3, stride=1, padding=1),
nn.BatchNorm(16),
nn.ReLU(),
)
self.conv5 = nn.Sequential(
# 定义一个卷积层conv5,输入通道是48,输出通道32,卷积核大小为1,步长为1,padding为0,使用relu激活函数
nn.Conv2D(48, 32, 1, stride=1, padding=0),
nn.BatchNorm(32),
nn.ReLU(),
)
self.conv6 = nn.Sequential(
# 定义一个卷积层conv6,输入通道是48,输出通道1,卷积核大小为3,步长为1,padding为1,使用relu激活函数
nn.Conv2D(48, 1, 3, stride=1, padding=1),
nn.BatchNorm(1),
)
self.relu = nn.ReLU()
# 定义网络的前向计算过程
def forward(self, inputs):
inputs = F.interpolate(x=inputs, scale_factor=[2,2], mode="bilinear")
x = self.conv1(inputs)
x1 = self.conv2(x)
x2 = self.conv3(x)
x = paddle.concat(x=[x1, x2], axis=1)
x3 = self.conv4(x)
x4 = self.conv5(x)
#张量连接操作,按列
x = paddle.concat(x=[x3, x4], axis=1)
#与输入图像维度保持一致
x = self.conv6(x)
#跨步连接,实现初值利用
x = x + inputs
x = self.relu(x)
#print("x.shape = {}".format(x.shape))
return x
if __name__ == '__main__':
img_channel = 1
width = 64
net = VRCNN()
inp = P.randn((1, 1, 120, 120),dtype='float32')
out = net(inp)
print('inp',inp.shape)
print('out',out.shape)
3、EDSR(2017)
①模型结构
模块化、残差连接、sub_pixel
②代码实现
from paddle.nn import Layer
from paddle import nn
import math
#超参定义
n_feat = 64
kernel_size = 3
# 残差块 尺寸不变
class _Res_Block(nn.Layer):
def __init__(self):
super(_Res_Block, self).__init__()
self.res_conv = nn.Conv2D(n_feat, n_feat, kernel_size, padding=1)
self.relu = nn.ReLU()
def forward(self, x):
y = self.relu(self.res_conv(x))
y = self.res_conv(y)
y *= 0.1
# 残差加入
y = paddle.add(y, x)
return y
class EDSR(nn.Layer):
def __init__(self):
super(EDSR, self).__init__()
in_ch = 1
num_blocks = 32
self.conv1 = nn.Conv2D(in_ch, n_feat, kernel_size, padding=1)
# 扩大
self.conv_up = nn.Conv2D(n_feat, n_feat * 4, kernel_size, padding=1)
self.conv_out = nn.Conv2D(n_feat, in_ch, kernel_size, padding=1)
self.body = self.make_layer(_Res_Block, num_blocks)
# 上采样
self.upsample = nn.Sequential(self.conv_up, nn.PixelShuffle(2))
# 32个残差块
def make_layer(self, block, layers):
res_block = []
for _ in range(layers):
res_block.append(block())
return nn.Sequential(*res_block)
def forward(self, x):
out = self.conv1(x)
out = self.body(out)
out = self.upsample(out)
out = self.conv_out(out)
return out
if __name__ == '__main__':
img_channel = 1
width = 64
net = EDSR()
inp = P.randn((1, 1, 120, 120),dtype='float32')
out = net(inp)
print('inp',inp.shape)
print('out',out.shape)
4、SRResNet(2017)
① 模型结构
SRResNet 使用 Parametric ReLU 而不是 ReLU,ReLU 引入一个可学习参数帮助它适应性地学习部分负系数; SRResNet 使用了图像上采样方法,SRResNet 使用了子像素卷积层。
②代码实现
from paddle.nn import Layer
from paddle import nn
import math
# 是可以保持w,h不变的
class ConvolutionalBlock(nn.Layer):
"""
卷积模块,由卷积层, BN归一化层, 激活层构成.
"""
def __init__(self, in_channels, out_channels, kernel_size, stride=1, batch_norm=False, activation=None):
"""
:参数 in_channels: 输入通道数
:参数 out_channels: 输出通道数
:参数 kernel_size: 核大小
:参数 stride: 步长
:参数 batch_norm: 是否包含BN层
:参数 activation: 激活层类型; 如果没有则为None
"""
super(ConvolutionalBlock, self).__init__()
if activation is not None:
activation = activation.lower()
assert activation in {'prelu', 'leakyrelu', 'tanh'}
# 层列表
layers = list()
# 1个卷积层
layers.append(
nn.Conv2D(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=stride,
padding=kernel_size // 2))
# 1个BN归一化层
if batch_norm is True:
layers.append(nn.BatchNorm2D(num_features=out_channels))
# 1个激活层
if activation == 'prelu':
layers.append(nn.PReLU())
elif activation == 'leakyrelu':
layers.append(nn.LeakyReLU(0.2))
elif activation == 'tanh':
layers.append(nn.Tanh())
# 合并层
self.conv_block = nn.Sequential(*layers)
def forward(self, input):
output = self.conv_block(input)
return output
# w,h放大
class SubPixelConvolutionalBlock(nn.Layer):
def __init__(self, kernel_size=3, n_channels=64, scaling_factor=2):
super(SubPixelConvolutionalBlock, self).__init__()
# 首先通过卷积将通道数扩展为 scaling factor^2 倍
self.conv = nn.Conv2D(in_channels=n_channels, out_channels=n_channels * (scaling_factor ** 2),
kernel_size=kernel_size, padding=kernel_size // 2)
# 进行像素清洗,合并相关通道数据 放大了图像
self.pixel_shuffle = nn.PixelShuffle(upscale_factor=scaling_factor)
# 最后添加激活层
self.prelu = nn.PReLU()
def forward(self, input):
output = self.conv(input)
output = self.pixel_shuffle(output)
output = self.prelu(output)
return output
class ResidualBlock(nn.Layer):
"""
残差模块, 包含两个卷积模块和一个跳连.
"""
def __init__(self, kernel_size=3, n_channels=64):
"""
:参数 kernel_size: 核大小
:参数 n_channels: 输入和输出通道数(由于是ResNet网络,需要做跳连,因此输入和输出通道数是一致的)
"""
super(ResidualBlock, self).__init__()
# 第一个卷积块
self.conv_block1 = ConvolutionalBlock(in_channels=n_channels, out_channels=n_channels, kernel_size=kernel_size,
batch_norm=True, activation='PReLu')
# 第二个卷积块
self.conv_block2 = ConvolutionalBlock(in_channels=n_channels, out_channels=n_channels, kernel_size=kernel_size,
batch_norm=True, activation=None)
def forward(self, input):
"""
前向传播.
:参数 input: 输入图像集,张量表示,大小为 (N, n_channels, w, h)
:返回: 输出图像集,张量表示,大小为 (N, n_channels, w, h)
"""
residual = input # (N, n_channels, w, h)
output = self.conv_block1(input) # (N, n_channels, w, h)
output = self.conv_block2(output) # (N, n_channels, w, h)
output = output + residual # (N, n_channels, w, h)
return output
class SRResNet(nn.Layer):
# SRResNet(scaling_factor=2)
def __init__(self, large_kernel_size=9, small_kernel_size=3, n_channels=64, n_blocks=16, scaling_factor=2):
"""
:参数 large_kernel_size: 第一层卷积和最后一层卷积核大小
:参数 small_kernel_size: 中间层卷积核大小
:参数 n_channels: 中间层通道数
:参数 n_blocks: 残差模块数
:参数 scaling_factor: 放大比例
"""
super(SRResNet, self).__init__()
# 放大比例必须为 2、 4 或 8
scaling_factor = int(scaling_factor)
assert scaling_factor in {2, 4, 8}, "放大比例必须为 2、 4 或 8!"
# 第一个卷积块
self.conv_block1 = ConvolutionalBlock(in_channels=1, out_channels=n_channels, kernel_size=large_kernel_size,
batch_norm=False, activation='PReLu')
# 一系列残差模块, 每个残差模块包含一个跳连接
self.residual_blocks = nn.Sequential(
*[ResidualBlock(kernel_size=small_kernel_size, n_channels=n_channels) for i in range(n_blocks)])
# 第二个卷积块
self.conv_block2 = ConvolutionalBlock(in_channels=n_channels, out_channels=n_channels,
kernel_size=small_kernel_size,
batch_norm=True, activation=None)
# 放大通过子像素卷积模块实现, 每个模块放大两倍 log2 2=1
n_subpixel_convolution_blocks = int(math.log2(scaling_factor))
self.subpixel_convolutional_blocks = nn.Sequential(
*[SubPixelConvolutionalBlock(kernel_size=small_kernel_size, n_channels=n_channels, scaling_factor=2) for i
in range(n_subpixel_convolution_blocks)])
# 最后一个卷积模块
self.conv_block3 = ConvolutionalBlock(in_channels=n_channels, out_channels=1, kernel_size=large_kernel_size,
batch_norm=False, activation='Tanh')
def forward(self, lr_imgs):
"""
:参数 lr_imgs: 低分辨率输入图像集, 张量表示,大小为 (N, 3, w, h)
:返回: 高分辨率输出图像集, 张量表示, 大小为 (N, 3, w * scaling factor, h * scaling factor)
"""
output = self.conv_block1(lr_imgs) # (16, 3, 24, 24)
residual = output # (16, 64, 24, 24)
output = self.residual_blocks(output) # (16, 64, 24, 24)
output = self.conv_block2(output) # (16, 64, 24, 24)
output = output + residual # (16, 64, 24, 24)
output = self.subpixel_convolutional_blocks(output) # (16, 64, 24 * 2, 24 * 2)
sr_imgs = self.conv_block3(output) # (16, 3, 24 * 2, 24 * 2)
return sr_imgs
if __name__ == '__main__':
img_channel = 1
width = 64
net = SRResNet()
inp = P.randn((1, 1, 120, 120),dtype='float32')
out = net(inp)
print('inp',inp.shape)
print('out',out.shape)
七、模型训练
1、加载数据
#根据数据类型加载Mydataset类
train_dataset= MyDataset(mode='train')
valid_dataset= MyDataset(mode='valid')
test_dataset = MyDataset(mode='test')
2、模型实例化
#在pytorch中实例化不需要调用函数,注意实例化时输入模型所需参数
model1 = paddle.Model(Sub_Pixel_CNN(upscale_factor=SCALE, channels=1))
model2 = paddle.Model(VRCNN())
model3 = paddle.Model(EDSR())
3、开始训练
在此处有3种训练模型的方式:①脚本风格,直接从头到尾顺序执行。②函数风格,将训练部分代码包装成一个函数,然后调用执行。③类风格,使用torchkeras中定义的模型接口构建模型,并调用compile方法和fit方法训练模型,使用该形式训练模型非常简洁明了。
# 设置学习率
base_lr = 0.001
#非固定学习率设置
# lr = paddle.optimizer.lr.PolynomialDecay(base_lr, power=0.9, end_lr=0.0001,decay_steps=2000)
# lr = paddle.optimizer.lr.CosineAnnealingDecay(learning_rate=base_lr, T_max=10000*5, verbose=False)
#设置优化器
optimizer = paddle.optimizer.Adam(learning_rate=base_lr,parameters=model.parameters())
model.prepare(optimizer,
MY_loss(),
# L1_Charbonnier_loss(),
[PSNR(),SSIM(),MSE(),RMSE(),NRMSE()],
)
# 启动模型训练,指定训练数据集,设置训练轮次,设置每次数据集计算的批次大小,设置日志格式
model.fit(train_dataset,
epochs=EPOCH,
batch_size=64,
verbose=1,
callbacks=ModelCheckpoint(save_dir= Model_name+'_checkpoint'))
八、模型评估
1、加载最优模型
# 更改训练好的参数文件路径
path1 = './02/'+'Sub_Pixel_CNN'+'_checkpoint/32.034657869787154_0.9560916978525543.pdparams'
model1.load(path1)
2、验证集评估
model1.prepare(paddle.optimizer.Adam(learning_rate=0.001,parameters=model1.parameters()),
MY_loss(),
[PSNR(),SSIM(),MSE(),RMSE(),NRMSE()],
)
#在验证集上运行模型,并输出结果
eval_result = model1.evaluate(valid_dataset, verbose=1)
print(eval_result)
3、数据整理
九、模型推断
用单个图像进行测试,并输出重建结果进行对比验证
主要涉及到画图的步骤较多
1、单样本预测
# 查看MRI图像
import matplotlib.pyplot as plt
import random
predict_results1 = model1.predict(test_dataset)[0]
# 从验证集中取出一张图片
index = random.randint(0,len(predict_results1))
img, label = test_dataset[index]
image = img[0].numpy()
print(image.shape)
image = cv2.resize(image,(image.shape[0]*2,image.shape[1]*2),interpolation=cv2.INTER_CUBIC)
predict1 = predict_results1[index]
2、以图像输出结果
#绘出输入
plot_results(image,prefix='./32/6-1')
#绘出输出
plot_results(predict1[0][0],prefix='./32/6-2')
print('6-1',psnrC(image,label[0]))
print('6-2',psnrC(predict1[0][0],label[0]))
3、自选图片测试
import math
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
def psnr(img1, img2):
"""
PSMR计算函数
"""
mse = np.mean( (img1/255. - img2/255.) ** 2 )
if mse < 1.0e-10:
return 100
PIXEL_MAX = 1
return 20 * math.log10(PIXEL_MAX / math.sqrt(mse))
def plot_results(img, title='results', prefix='out'):
"""
画图展示函数
"""
img_array = np.asarray(img, dtype='float32')
img_array = img_array.astype("float32") / 255.0
fig, ax = plt.subplots()
im = ax.imshow(img_array[::-1], origin="lower")
plt.title(title)
axins = zoomed_inset_axes(ax, 2, loc=2)
axins.imshow(img_array[::-1], origin="lower")
x1, x2, y1, y2 = 200, 300, 100, 200
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
plt.yticks(visible=False)
plt.xticks(visible=False)
mark_inset(ax, axins, loc1=1, loc2=3, fc="none", ec="blue")
plt.savefig(str(prefix) + "-" + title + ".png")
plt.show()
def get_lowres_image(img, upscale_factor):
"""
缩放图片
"""
return img.resize(
(img.size[0] // upscale_factor, img.size[1] // upscale_factor),
Image.BICUBIC,
)
def upscale_image(model, img):
'''
输入小图,返回上采样三倍的大图像
'''
# 把图片复转换到YCbCr格式
ycbcr = img.convert("YCbCr")
y, cb, cr = ycbcr.split()
y = np.asarray(y, dtype='float32')
y = y / 255.0
img = np.expand_dims(y, axis=0) # 升维度到(1,w,h)一张image
img = np.expand_dims(img, axis=0) # 升维度到(1,1,w,h)一个batch
img = np.expand_dims(img, axis=0) # 升维度到(1,1,1,w,h)可迭代的batch
out = model.predict(img) # predict输入要求为可迭代的batch
out_img_y = out[0][0][0] # 得到predict输出结果
out_img_y *= 255.0
# 把图片复转换回RGB格式
out_img_y = out_img_y.reshape((np.shape(out_img_y)[1], np.shape(out_img_y)[2]))
out_img_y = Image.fromarray(np.uint8(out_img_y), mode="L")
out_img_cb = cb.resize(out_img_y.size, Image.BICUBIC)
out_img_cr = cr.resize(out_img_y.size, Image.BICUBIC)
out_img = Image.merge("YCbCr", (out_img_y, out_img_cb, out_img_cr)).convert(
"RGB"
)
return out_img
def main(model, img, upscale_factor=2):
# 读取图像
with open(img, 'rb') as f:
img = Image.open(io.BytesIO(f.read()))
# 缩小三倍
lowres_input = get_lowres_image(img, upscale_factor)
w = lowres_input.size[0] * upscale_factor
h = lowres_input.size[1] * upscale_factor
# 将缩小后的图片再放大三倍
lowres_img = lowres_input.resize((w, h))
# 确保未经缩放的图像和其他两张图片大小一致
highres_img = img.resize((w, h))
# 得到缩小后又经过 Efficient Sub-Pixel CNN放大的图片
prediction = upscale_image(model, lowres_input)
psmr_low = psnr(np.asarray(lowres_img), np.asarray(highres_img))
psmr_pre = psnr(np.asarray(prediction), np.asarray(highres_img))
# 展示三张图片
plot_results(lowres_img, "lowres")
plot_results(highres_img, "highres")
plot_results(prediction, "prediction")
print("psmr_low:", psmr_low, "psmr_pre:", psmr_pre)
总结
提示:这里对文章进行总结:
以上是利用深度学习实现MRI超分辨率全过程的代码,包括公开数据集的下载与解压、预处理与加载、常用超分辨率模型的搭建、模型训练与测试常用函数的定义、模型训练与测试以及模型推断等,对于初学者收益颇多,可以了解到基本的项目框架和各部分的作用,学习到代码中各函数的作用,如果不明白为什么在这个地方使用这个函数也没关系,相信经过更长一段时间的学习(深度学习实践课程学习、论文阅读)会掌握的更为深刻。