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

# 概要
深層学習を用いた、**動画像**における超解像手法である[VSRnet](https://ivpl.northwestern.edu/wp-content/uploads/2019/02/07444187.pdf)の実装したので、それのまとめの記事です。
Python + Tensorflow(Keras)で実装を行いました。
今回は、**2倍拡大**の超解像にチャレンジしました。

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

# 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. VSRnetの超解像アルゴリズム
VSRnetは、深層学習を用いた超解像手法です。
学習の段階から動画像のフレームを入力する、初めての超解像手法でもあります。

VSRnetは、3層の``Convolution``から構成されていますが、入力する3枚のフレームを結合する位置に応じてモデルが3つあります。
``architecture(a)``は、``Convolution``層に入力する前に結合します。
``architecture(b)``は、1回それぞれの入力フレームを``Convolution``層に入力した後に結合します。
``architecture(c)``は、2回それぞれの入力フレームを``Convolution``層に入力した後に結合します。

概要図を見ると分かりやすいので、以下で概要図を示します。（図は[論文](https://ivpl.northwestern.edu/wp-content/uploads/2019/02/07444187.pdf)から引用）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/766947/c067e36b-d0e4-213c-0d25-f943ee5efd09.png)

今回実装したVSRnetは、事前に``bicubic``で拡大処理を行います。

ちなみに、VSRnetは、単一画像における超解像手法のSRCNNがもとになっています。

# 3. 実装したアルゴリズム
今回、実装したVSRnetのモデルは以下のように組みました。(コードの一部を抽出)

```python
    def model_a(self):
        #input video frames
        input_list = self.input_LR_num * [None]
        for img in range(self.input_LR_num): 
            input_list[img] = Input(shape = (None, None, self.input_channels), name = "input_" + str((img)))

        new_input = Concatenate()(input_list)

        conv2d_0 = Conv2D(filters = 64, kernel_size = (9, 9), padding = "same", activation = "relu")(new_input)
        conv2d_1 = Conv2D(filters = 32, kernel_size = (5, 5), padding = "same", activation = "relu")(conv2d_0)
        conv2d_2 =  Conv2D(filters = self.input_channels, kernel_size = (5, 5), padding = "same")(conv2d_1)

        model = Model(inputs = input_list, outputs = conv2d_2)
        model.summary()

        return model

    def model_b(self):
        #input video frames
        input_list = self.input_LR_num * [None]
        for img in range(self.input_LR_num): 
            input_list[img] = Input(shape = (None, None, self.input_channels), name = "input_" + str((img)))

        #convolution each images
        conv2d_0_tminus1 = Conv2D(filters = 64, kernel_size = (9, 9), padding = "same", activation = "relu")(input_list[0])
        conv2d_0_t = Conv2D(filters = 64, kernel_size = (9, 9), padding = "same", activation = "relu")(input_list[0])
        conv2d_0_tplus1 = Conv2D(filters = 64, kernel_size = (9, 9), padding = "same", activation = "relu")(input_list[0])

        #concatenate each results
        new_input = Concatenate()([conv2d_0_tminus1, conv2d_0_t, conv2d_0_tplus1])

        #convolution
        conv2d_1 = Conv2D(filters = 32, kernel_size = (5, 5), padding = "same", activation = "relu")(new_input)
        conv2d_2 =  Conv2D(filters = self.input_channels, kernel_size = (5, 5), padding = "same")(conv2d_1)

        model = Model(inputs = input_list, outputs = conv2d_2)
        model.summary()

        return model

    def model_c(self):
        #input video frames
        input_list = self.input_LR_num * [None]
        for img in range(self.input_LR_num): 
            input_list[img] = Input(shape = (None, None, self.input_channels), name = "input_" + str((img)))

       #convolution each images
        conv2d_0_tminus1 = Conv2D(filters = 64, kernel_size = (9, 9), padding = "same", activation = "relu")(input_list[0])
        conv2d_0_t = Conv2D(filters = 64, kernel_size = (9, 9), padding = "same", activation = "relu")(input_list[0])
        conv2d_0_tplus1 = Conv2D(filters = 64, kernel_size = (9, 9), padding = "same", activation = "relu")(input_list[0])

        conv2d_1_tminus1 = Conv2D(filters = 32, kernel_size = (5, 5), padding = "same", activation = "relu")(conv2d_0_tminus1)
        conv2d_1_t = Conv2D(filters = 32, kernel_size = (5, 5), padding = "same", activation = "relu")(conv2d_0_t)
        conv2d_1_tplus1 = Conv2D(filters = 32, kernel_size = (5, 5), padding = "same", activation = "relu")(conv2d_0_tplus1)

        new_input = Concatenate()([conv2d_1_tminus1, conv2d_1_t, conv2d_1_tplus1])

        conv2d_2 =  Conv2D(filters = self.input_channels, kernel_size = (5, 5), padding = "same")(new_input)

        model = Model(inputs = input_list, outputs = conv2d_2)

        model.summary()

        return model
```

3層の``Convolution``層で構成されており、フレームの結合は``Concatenate``で行っています。

# 4. 使用したデータセット
今回は、データセットに[REDS](https://seungjunnah.github.io/Datasets/reds.html)を使用しました。
このデータセットは、動画像の超解像用のデータセットで、240種類の学習用データ、30種類の検証用データ、30種類のテスト用データの計300種類のデータセットです。
別の実装でもほとんどこのデータセットを使用しています。

パスの構造はこんな感じです。

```
train_sharp - 001 - フレーム100枚
            - 002 - フレーム100枚
            - ...
            
val_sharp   - 001 - フレーム100枚
            - 002 - フレーム100枚
            - ...
```
このデータを``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_VSRnet)からコードを一式ダウンロードして、カレントディレクトリにします。
Windowsのコマンドでいうとこんな感じ。

```
C:~/keras_VSRnet>
```

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

```python:main.py
    parser.add_argument('--train_height', type = int, default = 36, help = "Train data size(height)")
    parser.add_argument('--train_width', type = int, default = 36, help = "Train data size(width)")
    parser.add_argument('--test_height', type = int, default = 720, help = "Test data size(height)")
    parser.add_argument('--test_width', type = int, default = 1280, help = "Test data size(width)")
    parser.add_argument('--train_dataset_num', type = int, default = 10000, help = "Number of train datasets to generate")
    parser.add_argument('--test_dataset_num', type = int, default = 5, help = "Number of test datasets to generate")
    parser.add_argument('--train_cut_num', type = int, default = 10, help = "Number of train data to be generated from a single image")
    parser.add_argument('--test_cut_num', type = int, default = 1, help = "Number of test data to be generated from a single image")
    parser.add_argument('--train_path', type = str, default = "../../dataset/reds_train_sharp", help = "The path containing the train image")
    parser.add_argument('--test_path', type = str, default = "../../dataset/reds_val_sharp", help = "The path containing the test image")
```

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

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

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

```
C:~/keras_VSRnet>python main.py --mode test_datacreate
```

## ② 学習
次に学習を行います。
設定するパラメータの箇所は、epoch数と学習率、今回のモデルの層の数です。
まずは、`main.py`の22~24行目

```python:main.py
    parser.add_argument('--learning_rate', type = float, default = 1e-4, help = "Learning_rate")
    parser.add_argument('--BATCH_SIZE', type = int, default = 32, help = "Training batch size")
    parser.add_argument('--EPOCHS', type = int, default = 1000, help = "Number of epochs to train for")
```

後は、学習のパラメータをあれこれ好きな値に設定します。
今回はモデルごとに分けているので、ここでは`model_a`を例に見ていきます。
`main.py`の65~79行目のパラメータを調節します。

```python:main.py
        train_model = model.VSRnet().model_a()

        optimizers = tf.keras.optimizers.Adam(lr = args.learning_rate)
        train_model.compile(loss = "mean_squared_error",
                        optimizer = optimizers,
                        metrics = [psnr])

        train_model.fit({"input_0":train_x[0], "input_1":train_x[1], "input_2":train_x[2]},
                        train_y,
                        epochs = args.EPOCHS,
                        verbose = 2,
                        batch_size = args.BATCH_SIZE)

        os.makedirs("model", exist_ok = True)
        train_model.save("model/model_a.h5")
```

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

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

```
C:~/keras_VSRnet>python main.py --mode train_model_a
```

これで、学習が終わるとモデルが出力されます。
他のモデルの学習を行う場合は、`a`を変えると同様にできます。

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

```
C:~/keras_VSRnet>python main.py --mode evaluate_a
```

このコマンドで、画像を出力してくれます。
他のモデルの学習を行う場合は、`a`を`変えると同様にできます。

# 7. 結果
出力した画像の結果例を以下に示します。
**なお、今回は輝度値のみで学習を行っているため、カラー画像には対応していません。**

まず、今回検証に使用した画像は以下の通りです。

![0_high.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/766947/d4ac0148-2f17-6597-d786-1f27292e8932.jpeg)

``bicubic``とVSRnetの各構造のPSNRの数値は以下のようになりました。

|補間・拡大処理 |PSNR(dB)|
|---|---|
|bicubic |30.75  |
|VSRnet_model_a |**31.95**  |
|VSRnet_model_b |31.73  |
|VSRnet_model_c |31.67  |

論文では``model_b``の数値が最も高かったのですが、今回の実装では``model_a``が最も高い数値となりました。
しかし、``bicubic``よりは軒並み良い結果となりました。

これ以上数値を上げる場合には、データセットにオプティカルフローなどの動き補償の制約を加えたり、モデルのパラメータチューニングを行うとよさそうです。

最後に元画像・低解像度画像・生成画像の一部を並べて拡大表示してみます。
![vsrnet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/766947/95c04ffa-8d20-1917-e32f-d91e25ede772.png)

目視でも変化が確認できました。
今回は1dBくらいの差があったので目視でも分かりやすいかと思います。
2dBくらいの差があると、もっと変化が出て一目で分かると思います。

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

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



# 9. まとめ
今回は、動画像における超解像手法であるVSRnetを実装してみました。
動画像の超解像手法は複雑になりがちですが、シンプルな構造で実装はしやすかったです。

記事が長くなってしまいましたが、最後まで読んでくださりありがとうございました。

# 参考文献
・[Video Super-Resolution With Convolutional Neural Networks](https://ivpl.northwestern.edu/wp-content/uploads/2019/02/07444187.pdf)
　実装の参考にした論文。
・[画素数の壁を打ち破る 複数画像からの超解像技術](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/)
　今回使用したデータセット。