tensorflowで画像処理

[[訓練・データ不要]ディープラーニングフレームワークを使ったアニメの線画の自動生成](https://qiita.com/koshian2/items/f53088d1dedaa72f9dca)の二番煎じです。

今回のコードはhttps://github.com/Sakai0127/img_proc_tf にあります。
colaboratoryでのデモも作りました。フィルタサイズなどのパラメータを変えて試せるようにしてあります。[リンク](https://colab.research.google.com/github/Sakai0127/img_proc_tf/blob/master/imgproc_demo.ipynb)

使用ライブラリ
tensorflow 1.14.0
numpy 1.16.1
##tensorflow実装
　まず画像をグレースケール変換する必要があります。元記事では1x1の畳み込みか行列積を使っていましたが、tensorflowではtf.image.rgb_to_grayscaleという関数があるのでそれを使います。内部の処理は(ほぼ)同じです。元記事では変換の際に
Y = 0.299R + 0.587G + 0.114B
という式で変換していますが、tensorflow実装では、
Y = 0.2989R + 0.5870G + 0.1140B
で変換されます。
　また、この記事ではNHWCのデータ形式を前提にしています。もしchannel_first(NCHW)などのデータを場合はご注意を。

サンプルは例によってこの画像で。
![ufo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194245/c86f366e-e223-eba1-ba07-cac181bca379.jpeg)

###膨張(dilation)
まずは元記事でもあった膨張処理から。といってもmaxpoolingを行うだけです。

```python
def dilation(inputs, filtersize, name='dilation'):
    assert filtersize % 2 == 1, 'filtersize must be odd.'
    with tf.name_scope(name):
        x = tf.nn.max_pool(inputs, [1, filtersize, filtersize, 1], [1, 1, 1, 1], padding='SAME')
    return x
```

　ここではfitersizeが偶数のときに停止するようにしていますが、実はassert文を削除すれば偶数でも動きます。tensorflowでpadding='SAME'と指定すると入力と同じshapeになるように自動でpaddingを行ってくれるためです。ですがこういったケースでは各pixelの近傍画素を等価に扱う必要があるため偶数を弾くようにしました。
　以下は生成結果です。フィルタサイズは3(=8近傍)です。
![dilation.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194245/6c2c09aa-4c6b-a321-12f0-519b478b5a64.jpeg)

###収縮(erosion)
　収縮(erosion)は膨張(dilation)とは逆の処理になります。dilationがmax_poolingを行っていることから分かるように近傍のpixelから最大値をピックアップする処理です。つまり画像の中で明るい部分(白に近い画素)を強調する処理です。erosionは逆に画像の暗い部分を強調します。つまり近傍pixelから最小値をピックアップする処理になります。

```python
def erosion(inputs, filtersize, name='erosion'):
    assert filtersize % 2 == 1, 'filtersize must be odd.'
    with tf.name_scope(name):
        x = 1.0 - inputs
        x = tf.nn.max_pool(x, [1, filtersize, filtersize, 1], [1, 1, 1, 1], 'SAME')
        x = 1.0 - x
    return x
```

　tensorflowのpoolingはmaxpoolingとavgpoolingしかなく直接最小値を取り出すことはできません。(というかminimum poolingなんて聞いたことないんですが…)なのでここでは入力の大小を反転させてからmax_poolingを行い、また反転させるという処理を行っています。ここでは1.0から入力を減算していますが、これは入力が0以上1以下にであるという前提です。なのでGANのようなモデルで出力層にtanhを使っていて-1以上1以下に正規化している場合などはおかしいことになります。
　以下は生成結果です。フィルタサイズは3(=8近傍)です。膨張差分でいい感じに生成されるんだから、収縮差分でも同じような画像が生成されるのでは？という浅はかな考えからやってみましたが膨張のほうが線がはっきりしているように見えますね。
![erosion.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194245/e5492167-9cd6-fb6a-0078-93e40a95cfac.jpeg)

###Adaptive threshold
　これは画像の2値化処理です。やっていることはシンプルで閾値を設定してそれ以下を0、それより大きい値を1にしているだけです。ただのclipingですね。ではどこがadaptiveかというと閾値をピクセルごとに設定している点です。その閾値の設定ですがあるピクセルとその近傍の加重平均をとるだけです。そしてこれは畳み込みで簡単に表現できます。加重平均を計算する際のweightはガウスフィルタと平均値の2通り実装しました。

```python
def gaussian_kernel(ksize=3, sigma=1.3):
    assert ksize % 2 == 1, 'kernel size must be odd.'
    def gausian2d(x, y, sigma):
        z = np.exp(-(x**2 + y**2) / (2 * sigma**2)) / (2 * np.pi * sigma**2)
        return z
    x = y = np.linspace(-sigma, sigma, num=ksize, dtype=np.float32)
    x, y = np.meshgrid(x, y)
    z = gausian2d(x, y, sigma)
    kernel = z / np.sum(z)
    return kernel

def Adaptivethreshold(inputs, filtersize=3, threshold_type='gaussian', sigma=1.3, c=2.0):
    with tf.name_scope('adaptive_threshold'):
        if threshold_type == 'gaussian':
            kernel = tf.constant(gaussian_kernel(ksize=filtersize, sigma=sigma).reshape(filtersize, filtersize, 1, 1), dtype=tf.float32, name='kernel')
        else:
            kernel = tf.ones([filtersize, filtersize, 1, 1]) / (filtersize**2)
        mean = tf.nn.conv2d(inputs, kernel, [1, 1, 1, 1], 'SAME')
        threshold = mean - (c / 255.0)
        image = inputs - threshold
        image = tf.clip_by_value(image, 0.0, 1.0)
        image = tf.ceil(image)
    return image
```

　以下は生成画像です。フィルタサイズは5, ガウシアンフィルタ, 分散は1.3, c=2.0で生成しました。ノイズがありすぎですね。パラメータ次第でもう少しきれいにできるかもしれませんが、GANなどにそのまま使うのは厳しそうな感じです。
![threshold.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194245/4feae43f-e407-4da4-4bc2-3968da497010.jpeg)
ということで、以上です。