DeepLearningを用いた超解像手法/VDSRの実装

# 概要
深層学習を用いた、**単一画像**における超解像手法である[VDSR](https://arxiv.org/abs/1511.04587)の実装したので、それのまとめの記事です。
Python + Tensorflow(Keras)で実装を行いました。

なんでVDSRという名前なのかというと、``VGG-net``から着想を得たモデルだからということらしいです。

今回紹介するコードは[Github](https://github.com/morimoris/keras_VDSR)にも載せています。

# 1. 超解像のおさらい
超解像について簡単に説明をします。

超解像とは解像度の低い画像に対して、解像度を向上させる技術のことです。
ここでいう解像度が低いとは、画素数が少なかったり、高周波成分(輪郭などの鮮鋭な部分を表す)がないような画像のことです。
以下の図で例を示します。(図は[[論文]](https://www.jstage.jst.go.jp/article/itej/62/3/62_3_337/_pdf/-char/ja)より引用)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/766947/bc4327db-4199-28e3-d610-55cd0f286f17.png)
これは、超解像の説明をする時によく使われる画像です。

(a)は原画像、(b)は画素数の少ない画像を見やすいように原画像と同じ大きさにした画像、(c)は高周波成分を含まない画像の例です。
(b)と(c)は、荒かったりぼやけていたりしていると思います。
このような状態を解像度が低い画像といいます。

そして、**超解像はこのような解像度が低い画像に処理を行い、(a)のような精細な画像を出力することを目的としています。**

# 2. VDSRの超解像アルゴリズム
VDSRのアルゴリズムの概要図は以下の通りです。(図は[論文](https://arxiv.org/abs/1511.04587)から引用)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/766947/4684a56d-528b-c1b9-5716-59d615a94c2f.png)

Convolution層を多数重ねることで、学習をさせていますが、
最後に入力画像と畳み込み演算の結果を足し合わせる``Skip Connnection``を導入していることが大きな特徴です。

これを行う意図としては、多数の畳み込み演算によって生じる勾配消失を防ぎ、画像の特徴を失わないようにするためです。
最後に入力画像を足し合わせることで、画像の特徴は残しつつ処理を行うことができる、というわけです。

今回実装したVDSRは、事前に``bicubic``で補間処理をした画像を入力するので、意味合いとしては、高周波成分の復元が近いと思います。

# 3. 実装したアルゴリズム
今回、実装したVDSRの構造のコマンドラインは以下の通りです。

```
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_1 (InputLayer)            [(None, None, None,  0
__________________________________________________________________________________________________
conv2d (Conv2D)                 (None, None, None, 6 640         input_1[0][0]
__________________________________________________________________________________________________
conv2d_1 (Conv2D)               (None, None, None, 6 36928       conv2d[0][0]
__________________________________________________________________________________________________
conv2d_2 (Conv2D)               (None, None, None, 6 36928       conv2d_1[0][0]
__________________________________________________________________________________________________
conv2d_3 (Conv2D)               (None, None, None, 6 36928       conv2d_2[0][0]
__________________________________________________________________________________________________
conv2d_4 (Conv2D)               (None, None, None, 6 36928       conv2d_3[0][0]
__________________________________________________________________________________________________
conv2d_5 (Conv2D)               (None, None, None, 6 36928       conv2d_4[0][0]
__________________________________________________________________________________________________
conv2d_6 (Conv2D)               (None, None, None, 6 36928       conv2d_5[0][0]
__________________________________________________________________________________________________
conv2d_7 (Conv2D)               (None, None, None, 6 36928       conv2d_6[0][0]
__________________________________________________________________________________________________
conv2d_8 (Conv2D)               (None, None, None, 6 36928       conv2d_7[0][0]
__________________________________________________________________________________________________
conv2d_9 (Conv2D)               (None, None, None, 6 36928       conv2d_8[0][0]
__________________________________________________________________________________________________
conv2d_10 (Conv2D)              (None, None, None, 6 36928       conv2d_9[0][0]
__________________________________________________________________________________________________
conv2d_11 (Conv2D)              (None, None, None, 6 36928       conv2d_10[0][0]
__________________________________________________________________________________________________
conv2d_12 (Conv2D)              (None, None, None, 6 36928       conv2d_11[0][0]
__________________________________________________________________________________________________
conv2d_13 (Conv2D)              (None, None, None, 6 36928       conv2d_12[0][0]
__________________________________________________________________________________________________
conv2d_14 (Conv2D)              (None, None, None, 6 36928       conv2d_13[0][0]
__________________________________________________________________________________________________
conv2d_15 (Conv2D)              (None, None, None, 6 36928       conv2d_14[0][0]
__________________________________________________________________________________________________
conv2d_16 (Conv2D)              (None, None, None, 6 36928       conv2d_15[0][0]
__________________________________________________________________________________________________
conv2d_17 (Conv2D)              (None, None, None, 6 36928       conv2d_16[0][0]
__________________________________________________________________________________________________
conv2d_18 (Conv2D)              (None, None, None, 6 36928       conv2d_17[0][0]
__________________________________________________________________________________________________
conv2d_19 (Conv2D)              (None, None, None, 1 577         conv2d_18[0][0]
__________________________________________________________________________________________________
add (Add)                       (None, None, None, 1 0           input_1[0][0]
                                                                 conv2d_19[0][0]
==================================================================================================
Total params: 665,921
Trainable params: 665,921
Non-trainable params: 0
__________________________________________________________________________________________________

```
今回は、``Convolution``の数を20にしています。
また、最後に``Add``で、入力画像と畳み込み演算の結果を足し合わせて最終的な結果にしています。

# 4. 使用したデータセット
今回は、データセットに[DIV2K dataset](https://data.vision.ee.ethz.ch/cvl/DIV2K/)を使用しました。
このデータセットは、単一画像のデータセットで、学習用が800種、検証用とテスト用が100種類ずつのデータセットです。
今回は、学習用データと検証用データを使用しました。
パスの構造はこんな感じです。

```
train_sharp - 0001.png 
            - 0002.png 
            - ...
            - 0800.png
            
val_sharp   - 0801.png
            - 0802.png 
            - ...
            - 0900.png
```

このデータをBicubicで縮小したりしてデータセットを生成しました。

# 5. 画像評価指標PSNR
今回は、画像評価指標としてPSNRを使用しました。
PSNR とは `Peak Signal-to-Noise Ratio`(ピーク信号対雑音比) の略で、単位はデジベル (dB) で表せます。
PSNR は信号の理論ピーク値と誤差の2乗平均を用いて評価しており、8bit画像の場合、255(最大濃淡値)を誤差の標準偏差で割った値です。
今回は、8bit画像を使用しましたが、計算量を減らすため、全画素値を255で割って使用しました。
そのため、最小濃淡値が0で最大濃淡値が1です。
dB値が高いほど拡大した画像が元画像に近いことを表します。
PSNRの式は以下のとおりです。

```math
PSNR = 10\log_{10} \frac{1^2 * w * h}{\sum_{x=0}^{w-1}\sum_{y=0}^{h-1}(p_1(x,y) - p_2(x,y))^2 }
```

なお、$w$は画像の幅、$h$は画像の高さを表しており、$p_1$は元画像、$p_2$はPSNRを計測する画像を示しています。

# 6. コードの使用方法
このコード使用方法は、自分が執筆した別の実装記事とほとんど同じです。

## ① 学習データ生成
まず、[Github](https://github.com/morimoris/keras_VDSR)からコードを一式ダウンロードして、カレントディレクトリにします。
Windowsのコマンドでいうとこんな感じ。

```
C:~/keras_VDSR>
```

次に、`main.py`から生成するデータセットのサイズ・大きさ・切り取る枚数、ファイルのパスなどを指定します。
`main.py`の15~26行目です。
使うPCのメモリ数などに応じで、画像サイズや学習データ数の調整が必要です。

```python:main.py
    train_height = 64  #HR・LRのサイズ
    train_width = 64
    test_height = 720  #HR・LRのサイズ
    test_width = 1280

    train_dataset_num = 10000 #生成する学習データの数
    test_dataset_num = 10     #生成するテストデータの数
    train_cut_num = 10        #一組の動画から生成するデータの数
    test_cut_num = 1

    train_path = "../../dataset/DIV2K_train_HR"  #画像が入っているパス
    test_path = "../../dataset/DIV2K_valid_HR"```
```

指定したら、コマンドでデータセットの生成をします。

```
C:~/keras_VDSR>python main.py --mode train_datacreate
```
これで、`train_data_list.npz`というファイルのデータセットが生成されます。

ついでにテストデータも同じようにコマンドで生成します。コマンドはこれです。

```
C:~/keras_VDSR>python main.py --mode test_datacreate
```
## ② 学習
次に学習を行います。
設定するパラメータの箇所は、epoch数と学習率、今回のモデルの層の数です。
まずは、`main.py`の28~31行目

```python:main.py
    model_depth = 20  #モデルの層の数

    BATCH_SIZE = 64
    EPOCHS = 80
```

後は、学習のパラメータをあれこれ好きな値に設定します。81~95行目です。

今回は、モデルを使用するために、モデルの層の数を設定しないといけません。
また、``20 epoch``ごとに学習率を変更させる必要があるため、複数回に分けて学習をさせています。

```python:main.py
        train_model = model.VDSR(model_depth)

        for i in range(EPOCHS // 20):
            optimizers = tf.keras.optimizers.SGD(lr=0.01 * (0.1 ** i), momentum=0.9, decay=1e-4, nesterov=False)
            train_model.compile(loss = "mean_squared_error",
                            optimizer = optimizers,
                            metrics = [psnr])

            train_model.fit(train_x,
                        train_y,
                        epochs = 20,
                        verbose = 2,
                        batch_size = BATCH_SIZE)

        train_model.save("VDSR_model.h5")
```

optimizerは``momentum``、損失関数は``mean_squared_error``を使用しています。

学習はデータ生成と同じようにコマンドで行います。

```
C:~/keras_VDSR>python main.py --mode train_model
```

これで、学習が終わるとモデルが出力されます。

## ③ 評価
最後にモデルを使用してテストデータで評価を行います。
これも同様にコマンドで行いますが、事前に①でテストデータも生成しておいてください。

```
C:~/keras_VDSR>python main.py --mode evaluate
```

このコマンドで、画像を出力してくれます。

# 7. 結果
出力した画像はこのようになりました。
**なお、今回は輝度値のみで学習を行っているため、カラー画像には対応していません。**
対応させる場合は、modelのInputのchannel数を変えたり、データセット生成のchannel数を変える必要があります。

- 元画像
![2_high.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/766947/9ea7953c-7329-254d-77a6-56915095658e.jpeg)

- 低解像度画像
![2_low.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/766947/37ca4889-e61f-00ff-a9bf-40a2d66251db.jpeg)

- 生成画像
**PSNR:27.22**
![2_pred.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/766947/d7f36833-f71e-7021-3664-68f24e511758.jpeg)

画像の粗さが取れていることが分かります。
画素数は増えていないので、高周波成分の復元が意味合いとしては近いですね。

最後に元画像・低解像度画像・生成画像の一部を並べて表示してみます。
![VDSR.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/766947/aa0afb63-ac07-75a8-b9c8-6381ede7c4a8.png)

4倍なので、入力画像が歪んでいることもあり、なかなか綺麗にはなりません。


# 8. コードの全容
前述の通り、[Github](https://github.com/morimoris/keras_VDSR)に載せています。
pythonのファイルは主に3つあります。
各ファイルの役割は以下の通りです。

- data_create.py : データ生成に関するコード。
- model.py : 超解像のアルゴリズムに関するコード。
- main.py : 主に使用するコード。



# 9. まとめ
今回は、最近読んだ論文の[VDSR](https://arxiv.org/abs/1511.04587)を元に実装してみました。

前述したとおり、``VGG-net``に着想を得たモデルとなっています。
記事が長くなってしまいましたが、最後まで読んでくださりありがとうございました。

# 参考文献
・[Accurate Image Super-Resolution Using Very Deep Convolutional Networks Jiwon](https://arxiv.org/abs/1511.04587)
　今回実装の参考にした論文。
・[画素数の壁を打ち破る 複数画像からの超解像技術](https://www.jstage.jst.go.jp/article/itej/62/3/62_3_337/_pdf/-char/ja)
　超解像の説明のために使用。
・[DIV2K dataset](https://data.vision.ee.ethz.ch/cvl/DIV2K/)
　今回使用したデータセット。