单图像超分辨率重建示例代码解析

昨天发了单图像超分辨率重建示例,今天来对里面主要的Python代码进行讲解,如果有补充或指正,欢迎大家在评论区留言。PS:这里的代码不要直接复制粘贴使用,由于注释的关系可能会报错,建议到示例给出的git中直接下载。

目录

一、run.py

二、data.py

三、image.py

四、 experiment.py

五、layers.py 

六、metrics.py

七、models.py

八、paths.py

九、run_all.py

十、参考目录


一、run.py

import argparse
# argparse 是 Python 的命令行工具模块,用来解析从命令行输入的命令
# import argparse
# parser = argparse.ArgumentParser()
# parser.parse_args()
# 这三行是它最基本的用法,在执行 parse_args() 之前,所有追加到命令行的参数都不会生效,生效之后的
# 效果类似于python run.py -h(默认参数是-h)

from functools import partial
# functools 模块为高阶函数提供支持——作用于或返回函数的函数被称为高阶函数。在该模块看来,一切可调
# 用的对象均可视为本模块中所说的“函数”。
# partial 则是一个函数装饰器,调用 partial 对象就和调用被修饰的函数 func 相同,只不过调用 
# partial 对象时传入的参数个数通常少于调用 func 时传入的参数个数。 当一个函数 func 可以接收很多
# 参数,而某一次使用只需要更改其中的一部分参数,其他的某些参数都保持不变时, partial 对象就可以将
# 这些不变的对象冻结起来,这样调用 partial 对象时传入未冻结的参数, partial 对象调用 func 时连
# 同已经被冻结的参数一同传给 func 函数,从而简化了调用过程。如果调用 partial 对象时提供了更多的
# 参数,那么他们会被添加到 args 的后面,如果提供了更多的关键字参数,那么它们将扩展或覆写已经冻结
# 的关键字参数。
# 比如这个简单的函数使用 partial 对象创建一个 base 参数始终为 2 的 int()
# from functools import partial
# basetwo = partial(int, base=2)
# basetwo.__doc__ = 'Convert base 2 string to an int.'
# basetwo('10010')
# 这个新的 partial 对象 basetwo 能够将二进制的参数转化为十进制的整型结果,在调用这个 partial 对
# 象时只需要传入二进制的目标参数即可。

import json
# 导入json模块,用于解码json格式或将键值对编码成json格式。JSON(JavaScript Object Notation) 是
# 一种轻量级的数据交换格式,易于人阅读和编写。
# json.dumps 用于将 Python 对象编码成 JSON 字符串。
# data = [ { 'a' : 1, 'b' : 2, 'c' : 3, 'd' : 4, 'e' : 5 } ]
# json = json.dumps(data)
# print json
# 执行结果是
# [{"a": 1, "c": 3, "b": 2, "e": 5, "d": 4}]
# 使用参数让 JSON 数据格式化输出
# print json.dumps({'a': 'Runoob', 'b': 7}, sort_keys=True, indent=4(缩进), separators=
# (',', ': # ')(分离器))
# {
#    "a": "Runoob",
#    "b": 7
# }
# json.loads 用于解码 JSON 数据。该函数返回 Python 字段的数据类型。
# jsonData = '{"a":1,"b":2,"c":3,"d":4,"e":5}';
# text = json.loads(jsonData)
# print text
# 输出结果为
# {u'a': 1, u'c': 3, u'b': 2, u'e': 5, u'd': 4}

from keras import optimizers
# Keras是一个高层神经网络API,Keras由纯Python编写而成并基Tensorflow、Theano以及CNTK后端。
# Keras 为支持快速实验而生,能够把你的idea迅速转换为结果。它的特点是:简易和快速的原型设计
# (keras 具有高度模块化,极简,和可扩充特性);支持CNN和RNN,或二者的结合;无缝 CPU 和 GPU 切
# 换。它适用于2.7-3.6版本的 Python。
# Keras 的核心数据结构是“模型”,模型是一种组织网络层的方式。Keras 中主要的模型是 Sequential 模
# 型,Sequential 是一系列网络层按顺序构成的栈。
# optimizers 是优化器对象,是编译 Keras 模型必要的两个参数之一
# 我们先建立一个序列模型,建立一个优化器对象,然后再对其进行优化
# model = Sequential()
# 添加模型参数
# model.add(Dense(64, kernel_initializer='uniform', input_shape=(10,)))
# model.add(Activation('tanh'))
# model.add(Activation('softmax'))
# 建立优化器对象
# sgd = optimizers.SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
# SGD 随机梯度下降法
# lr:大或等于0的浮点数,学习率
# momentum:大或等于0的浮点数,动量参数
# decay:大或等于0的浮点数,每次更新后的学习率衰减值
# nesterov:布尔值,确定是否使用Nesterov动量
# model.compile(loss='mean_squared_error', optimizer=sgd)

from pathlib import Path
# pathlib 类库用来取代 sys.os.path,pathlib 中的 Path 类可以创建 path 路径对象, 属于比 
# os.path 更高抽象级别的对象。
# 其基本用法有:
# Path.iterdir()  #遍历目录的子目录或者文件
# Path.is_dir()  #判断是否是目录
# Path.glob()  #过滤目录(返回生成器)
# Path.resolve()  #返回绝对路径
# Path.exists()  #判断路径是否存在
# Path.open()  #打开文件(支持with)
# Path.unlink()  #删除文件或目录(目录非空触发异常)

from toolbox.data import load_set
from toolbox.models import get_model
from toolbox.experiment import Experiment
# toolbox 是源码作者单独写的一个工具文件,没有进行打包,之后会对其中的每个文件进行详细说明


parser = argparse.ArgumentParser()
# 建立解析器对象
parser.add_argument('param_file', type=Path)
# 为解析器对象添加属性
args = parser.parse_args()
# 解析命令行语句
param = json.load(args.param_file.open())
# 读入 json 文件中的参数

# 建立模型
scale = param['scale']
# 获取倍数参数
build_model = partial(get_model(param['model']['name']),
                      **param['model']['params'])
# 根据 json 文件中的模型参数构建模型
if 'optimizer' in param:
# 如果 json 文件中给出了 optimizer 的参数
    optimizer = getattr(optimizers, param['optimizer']['name'].lower())
    optimizer = optimizer(**param['optimizer']['params'])
    # 读取它并建立一个optimizer
else:
    optimizer = 'adam'
    # 否则建立一个默认的 AdamOptimizer 类型的优化器

# 读取已有的模型参数数据
load_set = partial(load_set,
                   lr_sub_size=param['lr_sub_size'],
                   lr_sub_stride=param['lr_sub_stride'])
# 从历史数据文件中读取模型大小及步幅

# 对模型进行训练
expt = Experiment(scale=param['scale'](倍数), load_set=load_set(数据集),
                  build_model=build_model(模型),optimizer=optimizer(优化器),
                  save_dir=param['save_dir'](保存文件名))
expt.train(train_set=param['train_set'](训练集), val_set=param['val_set'](结果集),
           epochs=param['epochs'](循环次数), resume=True(是否重复))

# 对模型进行评估,得到损失值和峰值信噪比等评估参数
for test_set in param['test_sets']:
    expt.test(test_set=test_set)

二、data.py

from functools import partial

import numpy as np
# NumPy 系统是 Python 的一种开源的数值计算扩展。这种工具可用来存储和处理大型矩阵,比 Python 自
# 身的嵌套列表(nested list structure)结构要高效的多(该结构也可以用来表示矩阵(matrix))
# NumPy(Numeric Python)提供了许多高级的数值编程工具,如:矩阵数据类型、矢量处理,以及精密的运
# 算库。专为进行严格的数字处理而产生。
# np是numpy的假名,在后续的代码中可以直接使用,如np.stack(xxx)

from keras.preprocessing.image import img_to_array
from keras.preprocessing.image import load_img
# keras.preprocessing.image 是对图像进行处理的一整个工具类
# load_img用于读入图像文件
# img_to_array用于将读入的图像文件转化为数组存储,约等于numpy.asarray
# image = img_to_array(image)的结果近似于
# [[[1,2,3],[1,2,3],...,[1,2,3]],...,[[1,2,3],[1,2,3],...,[1,2,3]]]

from toolbox.image import bicubic_rescale
from toolbox.image import modcrop
from toolbox.paths import data_dir
# 会在对应的文件中进行说明


def load_set(name, lr_sub_size=11, lr_sub_stride=5, scale=3):
# 函数load_set(名称,大小默认=11,步长默认=5,倍数默认=3),读入图像集
    hr_sub_size = lr_sub_size * scale
    # 重建后大小
    hr_sub_stride = lr_sub_stride * scale
    # 重建后步长
    lr_gen_sub = partial(generate_sub_images, size=lr_sub_size,
                         stride=lr_sub_stride)
    # 定义一个新函数
    hr_gen_sub = partial(generate_sub_images, size=hr_sub_size,
                         stride=hr_sub_stride)

    lr_sub_arrays = []
    hr_sub_arrays = []
    for path in (data_dir / name).glob('*'):
    # path/data_dir 目录下所有文件
        lr_image, hr_image = load_image_pair(str(path), scale=scale)
        lr_sub_arrays += [img_to_array(img) for img in lr_gen_sub(lr_image)]
        hr_sub_arrays += [img_to_array(img) for img in hr_gen_sub(hr_image)]
    x = np.stack(lr_sub_arrays)
    # stack(arrays, axis=0),增加一维,新维度下标为0,如:
    # a=[[1,2,3],[4,5,6]]
    # np.stack(a,axis=0)
    # 结果为[[1 2 3] [4 5 6]](即变成两行)
    # 如果axis=1,则变成两列[[1 4] [2 5] [3 6]]
    y = np.stack(hr_sub_arrays)
    return x, y


def load_image_pair(path, scale=3):
# 从路径下读取一张图像,倍数默认=3
    image = load_img(path)
    image = image.convert('YCbCr')
    # image.convert()将图片转换成不同的格式,PIL中有九种不同模式。分别为1(两值模式,非黑即
    # 白),L(灰度图像),P(8位彩色图像,由调色板查出),RGB(24位彩色图像),RGBA(32位彩色图
    # 像,其中8位表示alpha透明度),CMYK(32位彩色图像,它的每个像素用32个bit表示,青色、洋红、
    # 黄色、黑色),YCbCr(24位彩色图像,Y是指亮度分量,Cb指蓝色色度分量,而Cr指红色色度分量),
    # I(32位整型灰色图像),F(32位浮点灰色图像)。
    hr_image = modcrop(image, scale)
    lr_image = bicubic_rescale(hr_image, 1 / scale)
    # 自定义函数,在image.py中进行说明
    return lr_image, hr_image


def generate_sub_images(image, size, stride):
# 对像素点依次进行处理
    for i in range(0, image.size[0] - size + 1, stride):
        for j in range(0, image.size[1] - size + 1, stride):
            yield image.crop([i, j, i + size, j + size])
            # yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函        
            # 数,Python 解释器会将其视为一个 genrator,它不会执行任何函数代码,直到对其调用 
            # next()(在 for 循环中会自动调用 next())才开始执行。
            # yield 的好处是显而易见的,它把一个函数改写为一个 generator 就获得了迭代能力,比起
            # 用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。
            # image.crop(左,上,右,下),作用为从图片的最前头拷贝一部分图片

三、image.py

import numpy as np
from PIL import Image
# PIL(Python Imaging Library)是python语言中对图像处理方面的一个开源库,其主要功能模块为Image


def array_to_img(x, mode='YCbCr'):
# 将数据数组转换为mode类型的图片,x为输入数组,mode为输出类型默认为YCbCr型
    return Image.fromarray(x.astype('uint8'), mode=mode)
    # 返回修改格式后的图片


def bicubic_rescale(image, scale):
# 双三次插值重建函数。三次卷积插值是一种更加复杂的插值方式。该算法利用待采样点周围16个点的灰度值
# 作三次插值,不仅考虑到4 个直接相邻点的灰度影响,而且考虑到各邻点间灰度值变化率的影响。三次运算
# 可以得到更接近高分辨率图像的放大效果,但也导致了运算量的急剧增加。这种算法需要选取插值基函数来
# 拟合数据。
    if isinstance(scale, (float, int)):
        size = (np.array(image.size) * scale).astype(int)
    return image.resize(size, resample=Image.BICUBIC)


def modcrop(image, scale):
# 余差抠图函数
    size = np.array(image.size)
    size -= size % scale
    return image.crop([0, 0, *size])

四、 experiment.py

from functools import partial
from pathlib import Path
import time
# 导入时间相关库

from keras import backend as K
# backend 运行后端,这里使用的是tensorflow
from keras.callbacks import CSVLogger
# CSVLogger 是把训练轮结果数据流到 csv 文件的回调函数
# keras.callbacks.CSVLogger(filename, separator=',', append=False)
# filename: csv 文件的文件名,例如 'run/log.csv'。
# separator: 用来隔离 csv 文件中元素的字符串。
# append: True:如果文件存在则增加(可以被用于继续训练)。False:覆盖存在的文件。

from keras.callbacks import ModelCheckpoint
# 可以在每个训练期之后保存模型
# keras.callbacks.ModelCheckpoint(filepath, monitor='val_loss', verbose=0, 
#  save_best_only=False, save_weights_only=False, mode='auto', period=1)
# filepath: 字符串,保存模型的路径。
# monitor: 被监测的数据。
# verbose: 详细信息模式,0 或者 1 。
# save_best_only: 如果 save_best_only=True, 被监测数据的最佳模型就不会被覆盖。
# mode: {auto, min, max} 的其中之一。 如果 save_best_only=True,那么是否覆盖保存文件的决定就
#  取决于被监测数据的最大或者最小值。 对于 val_acc,模式就会是 max,而对于 val_loss,模式就需要
#  是 min,等等。 在 auto 模式中,方向会自动从被监测的数据的名字中判断出来。
# save_weights_only: 如果 True,那么只有模型的权重会被保存 (model.save_weights(filepath)), 
#  否则的话,整个模型会被保存 (model.save(filepath))。
# period: 每个检查点之间的间隔(训练轮数)。

from keras.optimizers import adam
# adam优化器
# keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, 
#  amsgrad=False)
# lr: float >= 0. 学习率。
# beta_1: float, 0 < beta < 1. 通常接近于 1。
# beta_2: float, 0 < beta < 1. 通常接近于 1。
# epsilon: float >= 0. 模糊因子. 若为 None, 默认为 K.epsilon()。
# decay: float >= 0. 每次参数更新后学习率衰减值。
# amsgrad: boolean. 是否应用此算法的 AMSGrad 变种,来自论文 "On the Convergence of Adam and #  Beyond"。

from keras.preprocessing.image import img_to_array

import matplotlib
# Matplotlib 是一个 Python 2D 绘图库,它可以在各种平台上以各种硬拷贝格式和交互式环境生成出具有
# 出版品质的图形。
matplotlib.use('Agg')
# 这句好像是必须的,删掉之后会报错,我看文档里面也没说为什么,暂且先不管它
import matplotlib.pyplot as plt
# 导入绘图工具pyplot

import numpy as np

import pandas as pd
# pandas 是基于 NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。Pandas 纳入了大量库和一
# 些标准的数据模型,提供了高效地操作大型数据集所需的工具。pandas 提供了大量能使我们快速便捷地处理
# 数据的函数和方法。你很快就会发现,它是使 Python 成为强大而高效的数据分析环境的重要因素之一。

from toolbox.data import load_image_pair
from toolbox.image import array_to_img
from toolbox.metrics import psnr
from toolbox.models import bicubic
from toolbox.paths import data_dir
# 源码作者编写的工具类,后续会继续说明


class Experiment(object):
# 定义Experiment类,这个类是整个源代码体系结构中最重要的一部分
    def __init__(self, scale=3, load_set=None, build_model=None,
                 optimizer='adam', save_dir='.'):
    # 初始化函数,建立一系列模型相关的文件保存数据,每一次建立的模型都保存为一个hdf5文件
        self.scale = scale# 倍数
        self.load_set = partial(load_set, scale=scale)# 图集
        self.build_model = partial(build_model, scale=scale)# 模型
        self.optimizer = optimizer# 优化器
        self.save_dir = Path(save_dir)# 保存目录
        self.save_dir.mkdir(parents=True, exist_ok=True)# 创建保存目录

        self.config_file = self.save_dir / 'config.yaml'# 创建config属性文件
        self.model_file = self.save_dir / 'model.hdf5'# 创建模型数据文件

        self.train_dir = self.save_dir / 'train'# 训练集目录
        self.train_dir.mkdir(exist_ok=True)# 建立训练集目录
        self.history_file = self.train_dir / 'history.csv'# 建立历史数据文件
        self.weights_dir = self.train_dir / 'weights'# 模型权重目录
        self.weights_dir.mkdir(exist_ok=True)# 建立模型权重目录

        self.test_dir = self.save_dir / 'test'# 测试集目录
        self.test_dir.mkdir(exist_ok=True)# 建立测试集目录

    def weights_file(self, epoch=None):
    # 创建并保存权值文件
        if epoch is None:
        # 循环次数为0
            return self.weights_dir / 'ep{epoch:04d}.hdf5'
        else:
        # 否则
            return self.weights_dir / f'ep{epoch:04d}.hdf5'

    @property
    def latest_epoch(self):
    # 读取最近的模型的数据
        try:
        # 如果文件存在,则读取并返回数据内容
            return pd.read_csv(str(self.history_file))['epoch'].iloc[-1]
        except (FileNotFoundError, pd.io.common.EmptyDataError):
        # 否则跳过
            pass
        return -1

    def _ensure_dimension(self, array, dim):
    # 检验矩阵维度是否符合要求
        while len(array.shape) < dim:
            array = array[np.newaxis, ...]
        return array

    def _ensure_channel(self, array, c):
    # 检验图像通道是否符合要求
        return array[..., c:c+1]

    def pre_process(self, array):
    # 预处理
        array = self._ensure_dimension(array, 4)# 不超过4维
        array = self._ensure_channel(array, 0)# 无额外通道
        return array

    def post_process(self, array, auxiliary_array):
    # 后处理
        array = np.concatenate([array, auxiliary_array[..., 1:]], axis=-1)
        # 拼接数组
        array = np.clip(array, 0, 255)
        # 规范化数组,clip(数组,最小值,最大值)
        return array

    def inverse_post_process(self, array):
    # 反后处理(PS:个人看起来跟预处理没有任何区别,不知道为什么要写这么一个函数)
        array = self._ensure_dimension(array, 4)
        array = self._ensure_channel(array, 0)
        return array

    def compile(self, model):
    # 建立模型
        model.compile(optimizer=self.optimizer, loss='mse', metrics=[psnr])
        # 以默认参数构造模型
        return model

    def train(self, train_set='91-image', val_set='Set5', epochs=1,
              resume=True):
    # 训练模型
        # 读入数据并对数据进行预处理
        x_train, y_train = self.load_set(train_set)
        x_val, y_val = self.load_set(val_set)
        x_train, x_val = [self.pre_process(x)
                          for x in [x_train, x_val]]
        y_train, y_val = [self.inverse_post_process(y)
                          for y in [y_train, y_val]]

        model = self.compile(self.build_model(x_train))
        model.summary()
        # 根据结果建立新模型

        # Save model architecture
        # Currently in Keras 2 it's not possible to load a model with custom
        # layers. So we just save it without checking consistency.
        self.config_file.write_text(model.to_yaml())
        # 将模型参数保存到yaml文件当中以待继续训练或使用

        # Inherit weights
        # 继承模型的参数,未达到指定的训练轮次则继续训练
        if resume:
            latest_epoch = self.latest_epoch
            if latest_epoch > -1:
                weights_file = self.weights_file(epoch=latest_epoch)
                model.load_weights(str(weights_file))
            initial_epoch = latest_epoch + 1
        else:
            initial_epoch = 0

        # Set up callbacks
        # 建立反馈,修改模型
        callbacks = []
        callbacks += [ModelCheckpoint(str(self.model_file))]
        callbacks += [ModelCheckpoint(str(self.weights_file()),
                                      save_weights_only=True)]
        callbacks += [CSVLogger(str(self.history_file), append=resume)]

        # Train
        # 对模型进行训练
        model.fit(x_train, y_train, epochs=epochs, callbacks=callbacks,
                  validation_data=(x_val, y_val), initial_epoch=initial_epoch)

        # Plot metrics history
        # 根据结果绘制历史数据曲线,即train->history.loss和history.psnr
        prefix = str(self.history_file).rsplit('.', maxsplit=1)[0]
        df = pd.read_csv(str(self.history_file))
        epoch = df['epoch']
        for metric in ['Loss', 'PSNR']:
            train = df[metric.lower()]
            val = df['val_' + metric.lower()]
            plt.figure()
            plt.plot(epoch, train, label='train')
            plt.plot(epoch, val, label='val')
            plt.legend(loc='best')
            plt.xlabel('Epoch')
            plt.ylabel(metric)
            plt.savefig('.'.join([prefix, metric.lower(), 'png']))
            plt.close()

    def test(self, test_set='Set5', metrics=[psnr]):
    # 测试模型
        print('Test on', test_set)
        image_dir = self.test_dir / test_set
        image_dir.mkdir(exist_ok=True)
        # 读入测试图片集,建立测试结果文件夹

        # Evaluate metrics on each image
        # 计算每张图片的psnr矩阵
        rows = []
        for image_path in (data_dir / test_set).glob('*'):
        # 获取指定目录下的所有文件
            rows += [self.test_on_image(str(image_path),
                                        str(image_dir / image_path.stem),
                                        metrics=metrics)]
            # 对图片文件进行测试
        df = pd.DataFrame(rows)
        # DataFrame是Python中Pandas库中的一种数据结构,它类似excel,是一种二维表,可以存放多种
        # 数据结构。

        # Compute average metrics
        # 计算平均值
        row = pd.Series()
        row['name'] = 'average'
        for col in df:
            if col != 'name':
                row[col] = df[col].mean()
        df = df.append(row, ignore_index=True)

        df.to_csv(str(self.test_dir / f'{test_set}/metrics.csv'))
        # 将结果保存至csv文件

    def test_on_image(self, path, prefix, suffix='png', metrics=[psnr]):
    # 对图片文件进行测试,得到loss值和psnr值
        # Load images
        lr_image, hr_image = load_image_pair(path, scale=self.scale)
        # 读入图像

        # Generate bicubic image
        # 初始化插值图像
        x = img_to_array(lr_image)[np.newaxis, ...]
        bicubic_model = bicubic(x, scale=self.scale)
        y = bicubic_model.predict_on_batch(x)
        # predict_on_batch()函数在一个 batch 的样本上对模型进行测试,返回模型在一个 batch 上
        # 的预测结果
        bicubic_array = np.clip(y[0], 0, 255)

        # Generate output image and measure run time
        # 初始化输出图像,并计算运行时间
        x = self.pre_process(x)
        model = self.compile(self.build_model(x))
        if self.model_file.exists():
            model.load_weights(str(self.model_file))
        start = time.perf_counter()
        # time.perf_counter()获取时间戳
        y_pred = model.predict_on_batch(x)
        end = time.perf_counter()
        output_array = self.post_process(y_pred[0], bicubic_array)
        output_image = array_to_img(output_array, mode='YCbCr')

        # Record metrics
        # 记录矩阵
        row = pd.Series()
        row['name'] = Path(path).stem
        row['time'] = end - start
        y_true = self.inverse_post_process(img_to_array(hr_image))
        for metric in metrics:
            row[metric.__name__] = K.eval(metric(y_true, y_pred))

        # Save images
        # 保存结果图片
        images_to_save = []
        images_to_save += [(hr_image, 'original')]
        images_to_save += [(output_image, 'output')]
        images_to_save += [(lr_image, 'input')]
        for img, label in images_to_save:
            img.convert(mode='RGB').save('.'.join([prefix, label, suffix]))

        return row

五、layers.py 

from keras.engine.topology import Layer
# 对于简单、无状态的自定义操作,你也许可以通过 layers.core.Lambda 层来实现。但是对于那些包含了
# 可训练权重的自定义层,你应该自己实现这种层。在 keras 中,每个层都是对象,可以通过 dir ( Layer 
# 对象)来查看具有哪些属性。建立一个层只需要实现三个方法:
# build(input_shape): 这是你定义权重的地方。这个方法必须设 self.built = True,可以通过调用 
#  super([Layer], self).build() 完成。
# call(x): 这里是编写层的功能逻辑的地方。你只需要关注传入 call 的第一个参数:输入张量,除非你希
#  望你的层支持 masking。
# compute_output_shape(input_shape): 如果你的层更改了输入张量的形状,你应该在这里定义形状变化
#  的逻辑,这让 Keras 能够自动推断各层的形状。

import numpy as np

import tensorflow as tf
# TensorFlow 是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在
# 图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。它
# 最初由 Google 大脑小组(隶属于 Google 机器智能研究机构)的研究员和工程师们开发出来,用于机器学
# 习和深度神经网络方面的研究,但这个系统的通用性使其也可广泛用于其他计算领域。


custom_layers = {}# 保存不同的层


class ImageRescale(Layer):
# 重定义图像大小类
    def __init__(self, scale, method=tf.image.ResizeMethod.BICUBIC,
                 trainable=False, **kwargs):
    # 初始化函数
        self.scale = scale
        self.method = method
        super().__init__(trainable=trainable, **kwargs)
        # 继承父类的__init__()方法

    def compute_size(self, shape):
    # 计算图像大小
        size = np.array(shape)[[1, 2]] * self.scale
        return tuple(size.astype(int))
        # 返回一个元组

    def call(self, x):
    # 修改图片大小
        size = self.compute_size(x.shape.as_list())
        return tf.image.resize_images(x, size, method=self.method)

    def compute_output_shape(self, input_shape):
    # 计算输出的形状
        size = self.compute_size(input_shape)
        return (input_shape[0], *size, input_shape[3])

    def get_config(self):
    # 得到对应的属性值
        config = super().get_config()
        config['scale'] = self.scale
        config['method'] = self.method
        return config


custom_layers['ImageRescale'] = ImageRescale
# 建立 ImageRescale 层


class Conv2DSubPixel(Layer):
# 二维亚像素点卷积类
    # 卷积层的建立可以参考这个网址的说明:https://arxiv.org/abs/1609.05158
    def __init__(self, scale, trainable=False, **kwargs):
        self.scale = scale
        super().__init__(trainable=trainable, **kwargs)

    def call(self, t):
        r = self.scale
        shape = t.shape.as_list()
        new_shape = self.compute_output_shape(shape)
        H, W = shape[1:3]
        C = new_shape[-1]
        t = tf.reshape(t, [-1, H, W, r, r, C])
        # 源码作者没有使用网站中给的转换方程4,而是使用了自己的方法,相当于将perm中的3、4进行了
        # 交换。他认为这样做更加自然,我并没有对此做验证。
        t = tf.transpose(t, perm=[0, 1, 3, 2, 4, 5])  # S, H, r, H, r, C
        t = tf.reshape(t, [-1, H * r, W * r, C])
        return t

    def compute_output_shape(self, input_shape):
        r = self.scale
        H, W, rrC = np.array(input_shape[1:])
        assert rrC % (r ** 2) == 0
        return (input_shape[0], H * r, W * r, rrC // (r ** 2))

    def get_config(self):
        config = super().get_config()
        config['scale'] = self.scale
        return config


custom_layers['Conv2DSubPixel'] = Conv2DSubPixel
# 建立 Conv2DSubPixel 层

六、metrics.py

from keras import backend as K
import numpy as np


def psnr(y_true, y_pred):
# 计算峰值信噪比psnr
    """Peak signal-to-noise ratio averaged over samples and channels."""
    mse = K.mean(K.square(y_true - y_pred), axis=(-3, -2))
    return K.mean(20 * K.log(255 / K.sqrt(mse)) / np.log(10))


def ssim(y_true, y_pred):
# 计算结构相似性ssim
    """structural similarity measurement system."""
    ## K1, K2 are two constants, much smaller than 1
    K1 = 0.04
    K2 = 0.06
    
    ## mean, std, correlation
    mu_x = K.mean(y_pred)
    mu_y = K.mean(y_true)
    
    sig_x = K.std(y_pred)
    sig_y = K.std(y_true)
    sig_xy = (sig_x * sig_y) ** 0.5

    ## L, number of pixels, C1, C2, two constants
    L =  33
    C1 = (K1 * L) ** 2
    C2 = (K2 * L) ** 2

    ssim = (2 * mu_x * mu_y + C1) * (2 * sig_xy * C2) * 1.0 / ((mu_x ** 2 + mu_y ** 2 + C1) * (sig_x ** 2 + sig_y ** 2 + C2))
    return ssim

七、models.py

from keras.layers import Conv2D
# 2维卷积
# conv2d(x, kernel, strides=(1, 1), border_mode='valid', dim_ordering='th', 
#  image_shape=None, filter_shape=None)
# kernel:卷积核张量
# strides:步长,长为2的tuple
# border_mode:“same”,“valid”之一的字符串
# dim_ordering:“tf”和“th”之一,维度排列顺序

from keras.layers import Conv2DTranspose
# 该层是转置的卷积操作(反卷积)。需要反卷积的情况通常发生在用户想要对一个普通卷积的结果做反方向
# 的变换。例如,将具有该卷积层输出shape的tensor转换为具有该卷积层输入shape的tensor。同时保留与
# 卷积层兼容的连接模式。当使用该层作为第一层时,应提供input_shape参数。例如input_shape = 
# (3,128,128)代表128*128的彩色RGB图像。
# keras.layers.convolutional.Conv2DTranspose(filters, kernel_size, strides=(1, 1), 
#  padding='valid', data_format=None, activation=None, use_bias=True, 
#  kernel_initializer='glorot_uniform', bias_initializer='zeros', 
#  kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, 
#  kernel_constraint=None, bias_constraint=None)
# filters:卷积核的数目(即输出的维度)
# kernel_size:单个整数或由两个个整数构成的list/tuple,卷积核的宽度和长度。如为单个整数,则表示#  在各个空间维度的相同长度。
# strides:单个整数或由两个整数构成的list/tuple,为卷积的步长。如为单个整数,则表示在各个空间维
#  度的相同步长。任何不为1的strides均与任何不为1的dilation_rate均不兼容
# padding:补0策略,为“valid”, “same” 。“valid”代表只进行有效的卷积,即对边界数据不处
#  理。“same”代表保留边界处的卷积结果,通常会导致输出shape与输入shape相同。
# activation:激活函数,为预定义的激活函数名(参考激活函数),或逐元素(element-wise)的Theano
#  函数。如果不指定该参数,将不会使用任何激活函数(即使用线性激活函数:a(x)=x)
# dilation_rate:单个整数或由两个个整数构成的list/tuple,指定dilated convolution中的膨胀比
#  例。任何不为1的dilation_rate均与任何不为1的strides均不兼容。
# data_format:字符串,“channels_first”或“channels_last”之一,代表图像的通道维的位置。该参数
#  是Keras 1.x中的image_dim_ordering,“channels_last”对应原本的“tf”,“channels_first”对应原
#  本的“th”。以128x128的RGB图像为例,“channels_first”应将数据组织为(3,128,128),
#  而“channels_last”应将数据组织为(128,128,3)。该参数的默认值是~/.keras/keras.json中设置的
#  值,若从未设置过,则为“channels_last”。
# use_bias:布尔值,是否使用偏置项
# kernel_initializer:权值初始化方法,为预定义初始化方法名的字符串,或用于初始化权重的初始化器。
#  参考initializers
# bias_initializer:权值初始化方法,为预定义初始化方法名的字符串,或用于初始化权重的初始化器。参
#  考initializers
# kernel_regularizer:施加在权重上的正则项,为Regularizer对象
# bias_regularizer:施加在偏置向量上的正则项,为Regularizer对象
# activity_regularizer:施加在输出上的正则项,为Regularizer对象
# kernel_constraints:施加在权重上的约束项,为Constraints对象
# bias_constraints:施加在偏置上的约束项,为Constraints对象

from keras.layers import InputLayer
# 我没有找到关于InputLayer的具体说明,但是它跟keras的输入张量tensor有关

from keras.models import Sequential
# 序列模型
# model = Sequential()
# model.add(Dense(32, input_shape=(500,)))
# model.add(Dense(10, activation='softmax'))
# model.compile(optimizer='rmsprop',
#       loss='categorical_crossentropy',
#       metrics=['accuracy'])
# optimizer:字符串(预定义优化器名)或优化器对象,参考优化器
# loss:字符串(预定义损失函数名)或目标函数,参考损失函数
# metrics:列表,包含评估模型在训练和测试时的网络性能的指标,典型用法是metrics=['accuracy']
# sample_weight_mode:如果你需要按时间步为样本赋权(2D权矩阵),将该值设为“temporal”。默认
#  为“None”,代表按样本赋权(1D权)。在下面fit函数的解释中有相关的参考内容。
# weighted_metrics: metrics列表,在训练和测试过程中,这些metrics将由sample_weight或
#  clss_weight计算并赋权
# target_tensors: 默认情况下,Keras将为模型的目标创建一个占位符,该占位符在训练过程中将被目标数
#  据代替。如果你想使用自己的目标张量(相应的,Keras将不会在训练时期望为这些目标张量载入外部的
#  numpy数据),你可以通过该参数手动指定。目标张量可以是一个单独的张量(对应于单输出模型),也可
#  以是一个张量列表,或者一个name->tensor的张量字典。
# kwargs:使用TensorFlow作为后端请忽略该参数,若使用Theano/CNTK作为后端,kwargs的值将会传递给 
#  K.function。如果使用TensorFlow为后端,这里的值会被传给tf.Session.run
# 模型在使用前必须编译,否则在调用fit或evaluate时会抛出异常

import tensorflow as tf

from toolbox.layers import ImageRescale
from toolbox.layers import Conv2DSubPixel


def bicubic(x, scale=3):
# 建立双三次插值模型,通过计算周边的16个点进行插值。x为输入图片,scale为比例
    model = Sequential()
    model.add(InputLayer(input_shape=x.shape[-3:]))
    model.add(ImageRescale(scale, method=tf.image.ResizeMethod.BICUBIC))
    return model


def srcnn(x, f=[9, 1, 5], n=[64, 32], scale=3):
# 建立超分辨卷积神经网络模型。f为卷积核大小,n为选取的维度,下同
    """Build an SRCNN model.

    See https://arxiv.org/abs/1501.00092
    """
    assert len(f) == len(n) + 1
    model = bicubic(x, scale=scale)# 先通过双三次插值放大图像
    c = x.shape[-1]
    for ni, fi in zip(n, f):# 将低分辨率图像输入三层卷积神经网络
        model.add(Conv2D(ni, fi, padding='same',
                         kernel_initializer='he_normal', activation='relu'))
    model.add(Conv2D(c, f[-1], padding='same',
                     kernel_initializer='he_normal'))
    return model


def fsrcnn(x, d=56, s=12, m=4, scale=3):
# 建立FSRCNN模型,是SRCNN模型的改进
# ·它并不是将三次插值后的图像当做输入,而是直接将LR图像丢入到网络中,最后选用deconv进行放大在映射
#  Layer进行了改进,先shrink再将其复原
# ·更多的映射layer和更小的kernel
# ·共享其中的mapping layer,如果需要训练不同的upscale model,最后仅需要fine-tuning最后的
#  deconvLayer
    """Build an FSRCNN model.

    See https://arxiv.org/abs/1608.00367
    """
    model = Sequential()
    model.add(InputLayer(input_shape=x.shape[-3:]))
    c = x.shape[-1]
    f = [5, 1] + [3] * m + [1]
    n = [d, s] + [s] * m + [d]
    for ni, fi in zip(n, f):
        model.add(Conv2D(ni, fi, padding='same',
                         kernel_initializer='he_normal', activation='relu'))
    model.add(Conv2DTranspose(c, 9, strides=scale, padding='same',
                              kernel_initializer='he_normal'))
    return model


def nsfsrcnn(x, d=56, s=12, m=4, scale=3, pos=1):
# 建立一个反卷积核不同的FSRCNN模型
    """Build an FSRCNN model, but change deconv position.

    See https://arxiv.org/abs/1608.00367
    """
    model = Sequential()
    model.add(InputLayer(input_shape=x.shape[-3:]))
    c = x.shape[-1]
    f1 = [5, 1] + [3] * pos
    n1 = [d, s] + [s] * pos
    f2 = [3] * (m - pos - 1) + [1]
    n2 = [s] * (m - pos - 1) + [d]
    f3 = 9
    n3 = c
    for ni, fi in zip(n1, f1):
        model.add(Conv2D(ni, fi, padding='same',
                         kernel_initializer='he_normal', activation='relu'))
    model.add(Conv2DTranspose(s, 3, strides=scale, padding='same',
                              kernel_initializer='he_normal'))
    for ni, fi in zip(n2, f2):
        model.add(Conv2D(ni, fi, padding='same',
                         kernel_initializer='he_normal', activation='relu'))
    model.add(Conv2D(n3, f3, padding='same',
                         kernel_initializer='he_normal'))
    return model


def espcn(x, f=[5, 3, 3], n=[64, 32], scale=3):
# 建立ESPCN模型,直接在低分辨率图像尺寸上提取特征,计算得到高分辨率图像
    """Build an ESPCN model.

    See https://arxiv.org/abs/1609.05158
    """
    assert len(f) == len(n) + 1
    model = Sequential()
    model.add(InputLayer(input_shape=x.shape[1:]))
    c = x.shape[-1]
    for ni, fi in zip(n, f):
        model.add(Conv2D(ni, fi, padding='same',
                         kernel_initializer='he_normal', activation='tanh'))
    model.add(Conv2D(c * scale ** 2, f[-1], padding='same',
                     kernel_initializer='he_normal'))
    model.add(Conv2DSubPixel(scale))
    return model


def get_model(name):
    return globals()[name]

八、paths.py

from pathlib import Path


repo_dir = Path(__file__).parents[1]
data_dir = repo_dir / 'data'

九、run_all.py

from pathlib import Path
from subprocess import run
# subprocess 是Python 2.4中新增的一个模块,它允许你生成新的进程,连接到它们的 input/output/error 
# 管道,并获取它们的返回(状态)码。这个模块的目的在于替换几个旧的模块和方法。
# run() 函数是 Python 3.5 中新增的函数。执行指定的命令,等待命令执行完成后返回一个包含执行结果的
# CompletedProcess 类的实例。

for param_file in Path('.').glob('*.json'):
# 获取当前路径下所有后缀名为json的文件
    print(f'Run {param_file.stem}')
    # 打印文件名称
    run(['python', 'run.py', str(param_file)])
    # 以python执行run.py,执行参数为名为param_file的文件

十、参考目录

因为在全文都有引用,故不在每个地方标注引用。

[1] Python 命令行工具 argparse 模块使用详解

[2] Python——functools

[3] Python JSON

[4] Keras:基于Python的深度学习库

[5] 优化器optimizers

[6] [pathlib]内置pathlib库的常用属性和方法

[7] [Keras] SGD 随机梯度下降优化器参数设置

[8] NumPy 教程

[9] Numpy中stack(),hstack(),vstack()函数详解

[10] Python图像处理库PIL中图像格式转换(一)

[11] Python yield 使用浅析

[12] Python第三方库matplotlib(2D绘图库)入门与进阶

[13] 【Python学习笔记】Pandas库之DataFrame

[14] TensorFlow中文社区

[15] 介绍几种常用的插值方法以及代码-双三次插值

[16] 超分辨率重建之SRCNN

[17] 图像超分辨率重建之SRCNN

[18] 【超分辨率】FSRCNN--Accelerating the Super-Resolution Convolutional Neural Network

[19] 深度学习超分辨率重建(三): TensorFlow—— ESPCN

[20] 论文笔记 —— SRCNN

[21] Python之系统交互(subprocess)

猜你喜欢

转载自blog.csdn.net/qq_39391192/article/details/86770732