皆さんこんにちは
お元気ですか。やっと書き切りました。
実は今年ももう少しです。
昨年度はカノジョを作成し、その前は友利奈緒を真面目に解析しています。
いやぁ時の流れは早いものです。
これを踏まえ、今年は何をしようか考えました。
Chainer Advent Calendar 25日目で、皆様の妄想力を具現化したいと思います。
妄想について
さて、考えてみて欲しい。妄想というものを
画像に不自然に暗い箇所、何か見にくいなぁと思う箇所。
ここはこんな感じになっていると推測しているのではないでしょうか。
そう、推測、いや、補完です。
さて、本題ですが、部分的にモザイクがかかった画像を見るとここにはこれがあると妄想するでしょう。
そんなわけで、本日、皆様の妄想を具現化する、つまり、モザイクを外すことを試みます。
モザイク処理外し
モザイク処理とは
モザイク処理は次のように定義されているようです。(Wikipedia)
モザイク処理(モザイクしょり)(英語: mosaic processing)・ピクセル化 (英語: pixelization)とは写真・画像・静止画・映像・動画において表示したくない部分をピクセル単位で見えにくくする映像処理。(by Wikipedia)
要は見えにくい部分を加工する処理です。モザイク処理自体は後で詳細を述べます。
モザイク処理の種類
モザイク処理は大別して2種類の方式があります。
モザイクは、非可逆圧縮、可逆圧縮の手法です。
非可逆圧縮は元に戻すことができない圧縮方式であり、モザイク画像から元画像を完全に復元することができません。
それに対し、可逆圧縮は元に戻すことが可能な方式であり、現在は非可逆圧縮が主流となっているそうです。
(可逆圧縮の代表例としてFLMASKと呼ばれるツールが昔あったらしい、同じ会社の人に教えてもらった。)
現在は、非可逆圧縮の方式が主流のため、本実験でも同様の圧縮方式をベースに実現します。
本手法で選択したモザイクの手法は次のとおりです。
- 黒いモザイク・・・画面の一部が黒いモザイクとなる。
- 中央値を用いたモザイク・・・ぼかしたい箇所をmedian filterにする。
- Gaussian Filterを使ったモザイク・・・ぼかしたい箇所にGaussian Filterを使う。
今回は実用性を重視するため、部分画像への適用も試みます。
これは、全体を隠さなければならないケースは少なく
部分的に見てはいけないものとして隠していることもあるからです。
乱数による埋込も画像に対し、完成品があまりに不自然なので利用しません。
データセットの収集
モザイク処理外しのアルゴリズム
モザイク処理外し
はじめの方で一般的なモザイクが非可逆圧縮であると説明しました。
そのため、厳密にはモザイク処理を外すのではなく、モザイクがかかった箇所を補完する処理を行います。
この補完処理ですが、生成モデルで利用される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を利用した画像補完アルゴリズムです。
最初に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部では次のことをしています。
- Discriminatorの誤差に必要な情報を計算(オリジナル画像)
- Generatorで画像を生成
- Discriminatorの誤差に必要な情報を計算(生成画像)
- 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の画像 何も表現できていません。
150epoch 少しずつ表現ができてきた?
400epoch あんまり変わらない。
GAN
そして、GANを利用した生成処理を実施します。
1 epoch 表現が残念なところからスタート
150epoch
おまけ
少しおもしろいのがデバッグ用画像です。
このデバッグ用画像はGeneratorの出力をそのまま表示している画像で、
元画像での上書きも何もしていません。
Generatorのみ学習した場合
GANの場合
入力されていないと思われるラブライブ画像(サンシャインから取ってきた)
元画像1
大量に生成したモザイク+復元画像1
元画像2
大量に生成したモザイク+復元画像2
思ったよりも綺麗に生成されていますね。
最後に
妄想力を完全に可視化するにはまだまだかかるようです。
結構ネタとして面白かったのでちょっと画像大きくしてやってみるのも面白いかなぁと思いました。
※何かバグや違い、こうすれば良い等あれば教えてください。