のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

クリスマスにChainerを使って妄想力を可視化した。

Sponsored Links

皆さんこんにちは
お元気ですか。やっと書き切りました。

実は今年ももう少しです。
昨年度はカノジョを作成し、その前は友利奈緒を真面目に解析しています。

いやぁ時の流れは早いものです。
これを踏まえ、今年は何をしようか考えました。

Chainer Advent Calendar 25日目で、皆様の妄想力を具現化したいと思います。

qiita.com

妄想について

さて、考えてみて欲しい。妄想というものを
画像に不自然に暗い箇所、何か見にくいなぁと思う箇所。
ここはこんな感じになっていると推測しているのではないでしょうか。

そう、推測、いや、補完です。
さて、本題ですが、部分的にモザイクがかかった画像を見るとここにはこれがあると妄想するでしょう。
そんなわけで、本日、皆様の妄想を具現化する、つまり、モザイクを外すことを試みます。

モザイク処理外し

モザイク処理とは

モザイク処理は次のように定義されているようです。(Wikipedia

モザイク処理(モザイクしょり)(英語: mosaic processing)・ピクセル化 (英語: pixelization)とは写真・画像・静止画・映像・動画において表示したくない部分をピクセル単位で見えにくくする映像処理。(by Wikipedia

要は見えにくい部分を加工する処理です。モザイク処理自体は後で詳細を述べます。

モザイク処理の種類

モザイク処理は大別して2種類の方式があります。
モザイクは、非可逆圧縮可逆圧縮の手法です。

非可逆圧縮は元に戻すことができない圧縮方式であり、モザイク画像から元画像を完全に復元することができません。
それに対し、可逆圧縮は元に戻すことが可能な方式であり、現在は非可逆圧縮が主流となっているそうです。
可逆圧縮の代表例としてFLMASKと呼ばれるツールが昔あったらしい、同じ会社の人に教えてもらった。)

現在は、非可逆圧縮の方式が主流のため、本実験でも同様の圧縮方式をベースに実現します。
本手法で選択したモザイクの手法は次のとおりです。

  1. 黒いモザイク・・・画面の一部が黒いモザイクとなる。
  2. 中央値を用いたモザイク・・・ぼかしたい箇所をmedian filterにする。
  3. Gaussian Filterを使ったモザイク・・・ぼかしたい箇所にGaussian Filterを使う。

今回は実用性を重視するため、部分画像への適用も試みます。
これは、全体を隠さなければならないケースは少なく
部分的に見てはいけないものとして隠していることもあるからです。

乱数による埋込も画像に対し、完成品があまりに不自然なので利用しません。

モザイク処理アルゴリズムの実装

本処理はPythonで実装します。

ランダムで64枚出力した画像は次の通りです。
f:id:tereka:20171108212215p:plain

データセットの収集

本画像処理で用いるデータセットを収集します。
今回はラブライブのデータを利用してモザイク除去の実験を行います。

Googliser

GoogliserはGoogle画像検索を用いて、画像を取得するソフトウェアです。
このソフトウェアを利用してラブライブのデータを集めます。

github.com

googliserはcloneして内部にあるgoogliser.shを動作させれば実行可能です。

./googliser.sh -p "矢澤にこ" --number 1000

モザイク処理外しのアルゴリズム

モザイク処理外し

はじめの方で一般的なモザイクが非可逆圧縮であると説明しました。
そのため、厳密にはモザイク処理を外すのではなく、モザイクがかかった箇所を補完する処理を行います。

この補完処理ですが、生成モデルで利用されるGANとSemantic Segmentationの技術を応用して実現します。
SIGGRAPH 2017で発表のあった「Globally and Locally Consistent Image Completion」を参考に実装します。

Globally and Locally Consistent Image Completion

Globally and Locally Consistent Image Completionは
Deep Learningを利用した画像補完アルゴリズムです。

f:id:tereka:20171216155459p:plain

最初にGeneratorを学習し、次にDiscriminatorを学習し、
最後にGANの方式でGenerator, Discriminatorの両方を学習し、
高速マーチング法と呼ばれるアルゴリズムで周囲を補完します。

内部でいくつか細かい手法はあるのですが、ネットワークの構造をそのまま利用し、
諸々の処理の実装をしていません。(高速マーチング法)

Generatorを学習する際には復元画像を生成し、Mask部の誤差により更新します。
解像度を落とさず、DilatedConvolutionで大域的な特徴を利用する箇所と後段のDiscriminatorでは、
全体の画像と部分画像(マスク)を入力とし、判定しています。

Chainerで構築したモザイク外しアルゴリズム

さて、作ってみました。結構長くなったので全体は後ほどgithubへ置いておきます。
モデルとアップロード部については次で記述します。
コードはあまりきっちり記載していないので、綺麗にするためにはもう少し改良が必要です。

画像は128x128で入力しています。(処理の都合)

Generator

まずは、Generatorのコードです。
ここで肝となるのは、Dilated Convolutionを使い、大域的な特徴を見ていること
そして、誤差計算部(__call__)の引数にmskを入れています。
マスクを共に入力とした場合にモザイク領域以外の誤差を0にしています。

class GLCICGenerator(chainer.Chain):
    def __init__(self):
        super(GLCICGenerator, self).__init__()
        with self.init_scope():
            self.conv0 = L.Convolution2D(4, 64, ksize=3, stride=1, pad=1)
            self.bn0 = L.BatchNormalization(64)

            self.conv1_1 = L.Convolution2D(64, 128, ksize=3, stride=2, pad=1)
            self.bn1_1 = L.BatchNormalization(128)
            self.conv1_2 = L.Convolution2D(128, 128, ksize=3, stride=1, pad=1)
            self.bn1_2 = L.BatchNormalization(128)

            self.conv2_1 = L.Convolution2D(128, 256, ksize=3, stride=2, pad=1)
            self.bn2_1 = L.BatchNormalization(256)
            self.conv2_2 = L.Convolution2D(256, 256, ksize=3, stride=1, pad=1)
            self.bn2_2 = L.BatchNormalization(256)
            self.conv2_3 = L.Convolution2D(256, 256, ksize=3, stride=1, pad=1)
            self.bn2_3 = L.BatchNormalization(256)
            self.conv2_4 = L.DilatedConvolution2D(256, 256, ksize=3, stride=1, pad=2, dilate=2)
            self.bn2_4 = L.BatchNormalization(256)
            self.conv2_5 = L.DilatedConvolution2D(256, 256, ksize=3, stride=1, pad=4, dilate=4)
            self.bn2_5 = L.BatchNormalization(256)
            self.conv2_6 = L.DilatedConvolution2D(256, 256, ksize=3, stride=1, pad=8, dilate=8)
            self.bn2_6 = L.BatchNormalization(256)
            self.conv2_7 = L.Convolution2D(256, 256, ksize=3, stride=1, pad=1)
            self.bn2_7 = L.BatchNormalization(256)
            self.conv2_8 = L.Convolution2D(256, 256, ksize=3, stride=1, pad=1)
            self.bn2_8 = L.BatchNormalization(256)

            self.deconv2_1 = L.Deconvolution2D(256, 128, ksize=4, stride=2, pad=1)
            self.debn2_1 = L.BatchNormalization(128)
            self.deconv2_2 = L.Convolution2D(128, 128, ksize=3, stride=1, pad=1)
            self.debn2_2 = L.BatchNormalization(128)

            self.deconv1_1 = L.Deconvolution2D(128, 128, ksize=4, stride=2, pad=1)
            self.debn1_1 = L.BatchNormalization(128)
            self.deconv1_2 = L.Convolution2D(128, 64, ksize=3, stride=1, pad=1)
            self.debn1_2 = L.BatchNormalization(64)

            self.deconv0 = L.Convolution2D(64, 3, ksize=3, stride=1, pad=1)

    def predict(self, x):
        h = F.relu(self.bn0(self.conv0(x)))
        h = F.relu(self.bn1_1(self.conv1_1(h)))
        h = F.relu(self.bn1_2(self.conv1_2(h)))

        h = F.relu(self.bn2_1(self.conv2_1(h)))
        h = F.relu(self.bn2_2(self.conv2_2(h)))
        h = F.relu(self.bn2_3(self.conv2_3(h)))
        h = F.relu(self.bn2_4(self.conv2_4(h)))
        h = F.relu(self.bn2_5(self.conv2_5(h)))
        h = F.relu(self.bn2_6(self.conv2_6(h)))
        h = F.relu(self.bn2_7(self.conv2_7(h)))
        h = F.relu(self.bn2_8(self.conv2_8(h)))

        h = F.relu(self.debn2_1(self.deconv2_1(h)))
        h = F.relu(self.debn2_2(self.deconv2_2(h)))
        
        h = F.relu(self.debn1_1(self.deconv1_1(h)))
        h = F.relu(self.debn1_2(self.deconv1_2(h)))

        return F.sigmoid(self.deconv0(h))

    def __call__(self, x, msk=None, t=None):
        h = self.predict(x)
        if msk is not None:
            h = msk * h
            t = msk * t
            loss = F.mean_squared_error(h, t)
            chainer.report({'loss': loss}, self)
            return loss
        else:
            return h
Discriminator

次にDiscriminatorです。特徴は入力が全体の画像とモザイク画像を中心とする
64x64の画像を入力としています。最後に結合し、出力を算出しています。

class GLCICDiscriminator(chainer.Chain):
    def __init__(self):
        super(GLCICDiscriminator, self).__init__()
        with self.init_scope():
            self.c0_l = L.Convolution2D(3, 32, 3, 2, 1)
            self.bn0_l = L.BatchNormalization(32)
            self.c1_l = L.Convolution2D(32, 64, 3, 2, 1)
            self.bn1_l = L.BatchNormalization(64)
            self.c2_l = L.Convolution2D(64, 128, 3, 2, 1)
            self.bn2_l = L.BatchNormalization(128)
            self.c3_l = L.Convolution2D(128, 256, 3, 2, 1)
            self.bn3_l = L.BatchNormalization(256)
            self.c4_l = L.Convolution2D(256, 512, 3, 2, 1)
            self.bn4_l = L.BatchNormalization(512)

            self.c0_g = L.Convolution2D(3, 16, 3, 2, 1)
            self.bn0_g = L.BatchNormalization(16)
            self.c1_g = L.Convolution2D(16, 32, 3, 2, 1)
            self.bn1_g = L.BatchNormalization(32)
            self.c2_g = L.Convolution2D(32, 64, 3, 2, 1)
            self.bn2_g = L.BatchNormalization(64)
            self.c3_g = L.Convolution2D(64, 128, 3, 2, 1)
            self.bn3_g = L.BatchNormalization(128)
            self.c4_g = L.Convolution2D(128, 256, 3, 2, 1)
            self.bn4_g = L.BatchNormalization(256)
            self.c5_g = L.Convolution2D(256, 512, 3, 2, 1)
            self.bn5_g = L.BatchNormalization(512)

            self.fc = L.Linear(None, 1)

    def __call__(self, x1, x2):
        h1 = F.leaky_relu(self.bn0_l(self.c0_l(x1)))
        h1 = F.leaky_relu(self.bn1_l(self.c1_l(h1)))
        h1 = F.leaky_relu(self.bn2_l(self.c2_l(h1)))
        h1 = F.leaky_relu(self.bn3_l(self.c3_l(h1)))
        h1 = F.leaky_relu(self.bn4_l(self.c4_l(h1)))

        h2 = F.leaky_relu(self.bn0_g(self.c0_g(x2)))
        h2 = F.leaky_relu(self.bn1_g(self.c1_g(h2)))
        h2 = F.leaky_relu(self.bn2_g(self.c2_g(h2)))
        h2 = F.leaky_relu(self.bn3_g(self.c3_g(h2)))
        h2 = F.leaky_relu(self.bn4_g(self.c4_g(h2)))
        h2 = F.leaky_relu(self.bn5_g(self.c5_g(h2)))

        concat_h = F.concat([h1, h2])
        return self.fc(concat_h)
Updater

最後に更新用のUpdaterです。
このUpdaterはDiscriminator更新とGAN更新用です。
本体はupdate_core関数になります。update_core部では次のことをしています。

  1. Discriminatorの誤差に必要な情報を計算(オリジナル画像)
  2. Generatorで画像を生成
  3. Discriminatorの誤差に必要な情報を計算(生成画像)
  4. updateで更新

メソッド名とその説明を次に掲載します。

メソッド名 説明
loss_dis 識別器(Discriminator)を更新
loss_gen 生成器(Generator)を更新
extract_img モザイク部近辺の画像を切り取るメソッド
extract_mosaic_area モザイク部近辺の画像を切り取るメソッド(バッチ的に処理する部分)
update_core 更新用の関数を呼び出すコア部分
class GLCICUpdater(chainer.training.StandardUpdater):
    def __init__(self, is_gen_training=True, alpha=4e-4, *args, **kwargs):
        self.gen, self.dis = kwargs.pop('models')
        self.is_gen_training = is_gen_training
        self.alpha = alpha
        super(GLCCICUpdater, self).__init__(*args, **kwargs)

    def loss_dis(self, dis, y_fake, y_real):
        batchsize = len(y_fake)
        L1 = F.sum(F.softplus(-y_real)) / batchsize
        L2 = F.sum(F.softplus(y_fake)) / batchsize

        loss = (L1 + L2) * self.alpha
        chainer.report({'loss': loss}, dis)
        return loss

    def loss_gen(self, gen, y_fake, x_fake, img_batch_variable, masks):
        batchsize = len(y_fake)
        h = masks * x_fake
        t = masks * img_batch_variable
        abs_pixel_loss = F.mean_squared_error(h, t)
        loss = (F.sum(F.softplus(-y_fake)) * self.alpha) / batchsize + abs_pixel_loss
        chainer.report({'loss': loss, 'pixel_loss': abs_pixel_loss}, gen)
        return loss

    def extract_img(self, img, bbox):
        while True:
            min_h = max(min(bbox[3], 127) - 64, 0)
            max_h = min(bbox[2], 63)

            min_w = max(min(bbox[1], 127) - 64, 0)
            max_w = min(bbox[0], 63)

            start_h = random.randint(min_h, max_h)
            end_h = start_h + 64
            start_w = random.randint(min_w, max_w)
            end_w = start_w + 64

            if start_h >= 0 and start_w >= 0 and end_w < img.shape[1] and end_h < img.shape[2]:
                return img[:, start_h: end_h, start_w: end_w]

    def extract_mosaic_area(self, images, bboxs):
        mosaic_region_imgs = []
        for fake_variable, bbox_variable in zip(images.data, bboxs):
            fake = chainer.cuda.to_cpu(fake_variable)
            bbox = chainer.cuda.to_cpu(bbox_variable)
            mosaic_region_img = self.extract_img(fake, bbox).transpose((1, 2, 0))
            mosaic_region_imgs.append(cv2.resize(mosaic_region_img, (64, 64)).transpose((2, 0, 1)))
        return mosaic_region_imgs

    def update_core(self):
        if self.is_gen_training:
            gen_optimizer = self.get_optimizer('gen')
        dis_optimizer = self.get_optimizer('dis')

        batch = self.get_iterator('main').next()
        img_batch, mosaic_batch_imgs, img_with_mask_batch, bbox_batch, masks = self.converter(batch, self.device)
        img_batch_variable = Variable(img_batch)
        x_real = Variable(img_with_mask_batch)
        xp = chainer.cuda.get_array_module(x_real.data)

        gen, dis = self.gen, self.dis

        region_real_images = self.extract_mosaic_area(img_batch_variable, bbox_batch)
        region_real_images_variable = Variable(xp.asarray(region_real_images))
        y_real = dis(region_real_images_variable, img_batch_variable)  # cut off image
        x_fake = gen(x_real)

        region_fake_images = self.extract_mosaic_area(x_fake, bbox_batch)
        region_fake_images_variable = Variable(xp.asarray(region_fake_images))
        y_fake = dis(region_fake_images_variable, x_fake)

        dis_optimizer.update(self.loss_dis, dis, y_fake, y_real)
        if self.is_gen_training is True:
            gen_optimizer.update(self.loss_gen, gen, y_fake, x_fake, img_batch_variable, masks)

結果

Generator

まずは、Generator部を利用しました。
Googliserで画像は収集し、その中から一部をテスト用としたので、検索が被っていれば重複の可能性があります。ご了承ください。

1epochの画像 何も表現できていません。
f:id:tereka:20171225212515j:plain:w640,h640

150epoch 少しずつ表現ができてきた?
f:id:tereka:20171225212923j:plain:w640,h640

400epoch あんまり変わらない。
f:id:tereka:20171225212953j:plain:w640,h640

GAN

そして、GANを利用した生成処理を実施します。
1 epoch 表現が残念なところからスタート
f:id:tereka:20171225213334j:plain:w640,h640

150epoch
f:id:tereka:20171225213434j:plain:w640,h640

おまけ

少しおもしろいのがデバッグ用画像です。
このデバッグ用画像はGeneratorの出力をそのまま表示している画像で、
元画像での上書きも何もしていません。

Generatorのみ学習した場合
f:id:tereka:20171225213716j:plain:w640,h640

GANの場合
f:id:tereka:20171225220845j:plain:w640,h640

入力されていないと思われるラブライブ画像(サンシャインから取ってきた)

元画像1
f:id:tereka:20171225233213j:plain:w256,h256
大量に生成したモザイク+復元画像1
f:id:tereka:20171225233204j:plain:w640,h640

元画像2
f:id:tereka:20171225233532j:plain:w256,h256

大量に生成したモザイク+復元画像2
f:id:tereka:20171225233521j:plain:w640,h640

思ったよりも綺麗に生成されていますね。

最後に

妄想力を完全に可視化するにはまだまだかかるようです。
結構ネタとして面白かったのでちょっと画像大きくしてやってみるのも面白いかなぁと思いました。
※何かバグや違い、こうすれば良い等あれば教えてください。