「ガラスに映った世界なんていらない」とニューラルネットに祈るには

はじめに

毎月1記事は頑張って書こうと決意表明するも、結局休日にブログを全然書かずに家でぼーっと無になりながらアニメを鑑賞してるだけで何もしてませんでした。

戒めのために有言不実行Tシャツでも作って着ようかなと思いましたが、それもきっと作らないので、本当に厄介ですね。

さて、話はそれましたが、今回取り上げるタスクはガラスの反射除去で、論文としては2018年1月31日にarxivに投稿された「Single Image Reflection Removal Using Deep Encoder-Decoder Network」を取りあげます。結構古いですが、この時点でのstate-of-the-artの2つのモデルを叩きのめした手法とのことで、実装が比較的簡単そうだったので、これにしました。ただ、実験し始めてから気づいたのですが、どこの学会にも通してなさそうなので、微妙だったかもしれません。

このタスクを選んだモチベーションは、結構前に水族館に行った際に以下のような感じで、ガラスに反射した無粋な誘導灯が写り込んでしまったせいで台無しになってしまっていたのをDeep Learningってやつでなんとかできませんかと思ったからです。

f:id:gat-chin321:20190511160943j:plain
水族館でとったペンギンの画像

概要

この論文での学習までの流れは以下の通りです。

1. 写したい風景の画像(透過画像)とガラスに反射させたい画像(反射画像)の2枚の画像を使って、ガラスに反射した物体の映る合成画像(反射合成画像)を擬似的に作成する(データの準備)
2. 反射合成画像を入力にして、反射合成画像から復元した反射画像を減算して合成に利用した透過画像を復元した時のlossでネットワークを学習させる

それではデータの準備方法から見ていきましょう。

データの準備(前処理?)

合成画像の作り方

最初にガラスの反射除去をニューラルネットに学習させるためのガラスの反射なし画像と反射あり画像のデータセットをどうやって用意するのかが多分気になると思います。

以下のパターンが方法としては考えられます。

  • カメラの位置を固定して対象の物体の前にガラスを設置したケースとしなかったケースで撮影する
  • 2枚の画像を合成して、擬似的にガラスに物体が写り込んだ画像を再現する

主流なのは後者で、理由としては前者だと映り込む物体の種類を揃えるのが大変なので圧倒的に少なくなってしまう点と日光のあたり具合による変化などのノイズが混ざる点と例えば動くもの(動物とか風に揺れる木とか)を対象にする方法ではそもそも写したい物体が移動してしまう点とそもそもデータセットの作成コストが高い点など色々な問題があるからです。

この記事で紹介する論文では、2枚の画像を合成した画像を生成することでガラスの反射なし画像と反射あり画像のデータセットを作成しています。

この後者の合成の数式で一般的に利用されている(とこの論文で主張している)のは以下になります。

 {
\displaystyle
\begin{eqnarray}
I = \alpha T + \beta R + n
\end{eqnarray}
}

 I: 入力用の合成画像
 \alpha: 写したい風景の画像(透過画像) Tの透過率
 T: 写したい風景の画像(透過画像)
 \beta: 反射させたい画像 Rの反射率
 R: 反射させたい画像(反射画像)
 n: ノイズ項

上記の式を変形すると、合成画像 Iとその反射のない画像 Tとの間の差は以下のようにノイズ項 nの一部として見ることができます。

 {
\displaystyle
\begin{eqnarray}
\beta R + n = I - \alpha T
\end{eqnarray}
}

そして、このように合成画像にノイズを加える方法だと、自然な画像と同様の特性を持つ(よくわかりませんが、普通に写真撮ったときに生じるノイズと変わらないということ?)なので、反射画像 Rからノイズ nを正確に区別することが困難になってしまうらしいです。

この論文でも、一般的な合成の式と同様に合成画像 Iを2つの無反射自然画像 T Rの線形結合として解釈します。

ただ、合成画像の合成を正確にシミュレートするために、反射におけるぼけ効果を考慮します。

どのように考慮するかというと、ほとんどの実際の写真では普通はガラスの背後にあるものが関心のある物体なので、カメラの焦点面は反射された物体ではなくガラスの背後にある物体上にあるはずです。

その結果、デフォーカス効果のために反射がぼやけていることがよくあります。合成画像においてこの効果をシミュレートするために、それを合成画像に重ね合わせる前に、反射画像 Rをランダム分散のガウシアンカーネル Gでぼかします。

 {
\displaystyle
\begin{eqnarray}
I = \alpha T + \beta R * G
\end{eqnarray}
}

 G: ランダムな分散を利用するガウシアンカーネル

また、この論文では以下の図のように、反射した対象が2倍に重なって見えるdouble reflection効果という現象も考慮しています。

f:id:gat-chin321:20190511184334p:plain
論文より引用したdouble reflection効果の例(例なのに合成画像だが…)

このdouble reflection効果は以下のような形で反射する物体がガラスの表面とガラスから光が出る面の2箇所で反射した際に発生する現象らしいです。

f:id:gat-chin321:20190511183953p:plain
double reflection効果の図(論文より引用)

これはどのように考慮するのかというと以下の式で表現するそうですが、振幅 1-\sqrt{\alpha}および \sqrt{\alpha}の2つのパルスを有するランダムカーネル Kで反射画像Rを畳み込むと書いていましたが、このランダムカーネル Kが全く理解できませんでした。

 {
\displaystyle
\begin{eqnarray}
I = \alpha T + \beta R * G * K
\end{eqnarray}
}

何をするのか理解できてないものについては実装できないという点とそもそも目的として掲げた水族館で撮った画像はdouble reflection効果が発生していないので、別に考慮する必要ないという点から今回は実装しません。

JPEGを使って学習させるための工夫

画像合成を行う場合、JPEG圧縮画像に直接適用することはできません。

その理由は人間の非線形光感度を利用するためには、JPEG画像はカメラに保存される前にガンマ補正されているので、JPEG画像の画素値は画像センサによって捕捉された光強度に対して線形ではありません。

その結果、2つのガンマ補正された画像の直接合計は式(1)によって要求されるような光の重ね合わせの物理学に一致せず、非現実的な反射画像を発生させてしまいます。

この問題を解決するには、生の画像のみを使用するか、収集したJPEG画像に以下の逆ガンマ補正を適用します。

 {
\displaystyle
\begin{eqnarray}
X = (X')^{\frac{1}{r}}
\end{eqnarray}
}

 X': ガンマ補正画像
 X: ガンマ補正された画像
 r: ガンマ補正の係数

各カラーチャンネルのガンマ補正係数は、各JPEG画像に添付されているEXIFのセグメントをみると利用できることが多いようです。

Deep Encoder-Decoder Network

ネットワークのアーキテクチャ

大まかなアーキテクチャとしては論文にある以下の図がわかりやすいかと思います。見た目的にはU-Netっぽい感じの構造をしています。

f:id:gat-chin321:20190511193327p:plain
Deep Encoder-Decoder Networkのアーキテクチャ(論文より引用)

この論文で提案されているネットワークは以下のように3つの段階に分かれています。

  • Feature extraction(特徴抽出)
    • 透過を復元するための層と反射を復元するための層の両方で利用するために、6つのConvolution層を使って特徴量を抽出します。
  • Reflection recovery and removal(反射の復元と除去)
    • この段階における最初の6個のConvolution層とその先の6個のDeconvolution層は反射を学習し復元するために追加しています。
    • 反射層の詳細を良い感じに保存する目的で、2つ目のskip connectionによって前の畳み込み層から学んだ特徴を継承させています。
    • 要素ごとの減算とそれに続くReLU( max(0, conv_6 - deconv_6))によって、復元された反射が deconv_7(7番目のDeconvolution層)の前に除去される仕組みになっています。
  • Transmission layer restoration(透過レイヤーの復元)
    • 反射は推定反射を単純に差し引くことによって、前の段階の後で完全には除去されない場合があります。
    • したがって、この段階は反射減算画像から視覚的に好ましい透過画像を復元するために、ターゲットシーンの特徴から透過層を6つのDeconvolution層を用います。

loss関数の設計

ノイズ除去、ピンボケ除去および超解像などの多くのニューラルネットワークに基づく画像復元技術では、ネットワークは一般に損失関数として出力とground truthとの間の平均二乗誤差(MSE)を用いて最適化することが多いため、この論文では平均二乗誤差を採用している。

 {
\displaystyle
\begin{eqnarray}
L_{l2} = || F(I) - \alpha T ||_{2}^{2}
\end{eqnarray}
}

 \alpha: 写したい風景の画像(透過画像) Tの透過率
 T: 写したい風景の画像(透過画像)
 F: 特徴を抽出するための関数(特徴抽出器)

ただし、L2ノルムのloss関数のみを使用して最適化されたモデルでは、高周波成分を保存できないことがよくあるそうです。

反射除去の場合、反射層と透過層はどちらも異なる特性を持つ自然画像となります。

そのため、最良の復元結果を得るには、ネットワークは透過層の特性を上手く学習する必要があります。

VGG lossでは事前に訓練された19層のVGGネットワ​​ークの特徴空間上における、復元された透過層表現 T'=F(I)と実際の透過画像 Tの差のL2ノルムとして以下のように計算します。この論文のモデルでは最初の5つの畳み込み層の特徴マップとして使用します(そのため、M=5)。

 {
\displaystyle
\begin{eqnarray}
L_{VGG} = \frac{1}{W_{i}H_{i}} \sum^{M}_{i=1}|| \phi_{i}(\alpha T) - \phi_{i}(F(I)) ||_{2}^{2}
\end{eqnarray}
}

 W_{i}: 特徴マップのi番目のw方向の次元数
 H_{i}: 特徴マップのi番目のh方向の次元数
 M: 使用されるConvolution層の数 (この論文のモデルではM=5)
 \phi_{i}: 訓練済みVGG19のネットワークの中の活性化関数の後のConvolution層


学習の最終的な損失は以下のように計算されます。

 {
\displaystyle
\begin{eqnarray}
L = L_{l2} + \lambda L_{VGG}
\end{eqnarray}
}
 \lambda: L2 lossとVGG lossのバランスをとるためのハイパーパラメータ

ネットワークの訓練方法

 {
\displaystyle
\begin{eqnarray}
\theta_{F} = argmin_{\theta_F} \frac{1}{N} \sum_{n=1}^{N} L(F(I_n), \alpha T_n)
\end{eqnarray}
}

 N: 訓練集合のサイズ
 I_n: n枚目の反射を合成した画像
 T_n: n枚目の写したい風景の画像(透過画像)

学習用のハイパーパラメータ

  • すべての畳み込み層および逆畳み込み層に対して64個のフィルタを設定。
  • カーネルサイズ: 5×5(ネットワークに画像の意味的文脈を学習させるため)
  • VGG lossの重みは \lambda = 0.001
  • 最適化アルゴリズム: Adam
    • 学習率:  10^4
    •  beta_1 = 0.9
  • 合成画像生成時の工夫
    • 透過率 \alphaの値域は0.75~0.8のランダム
    • Gaussian blur kernelの分散は1から5のランダム
    • 合成画像を生成する前に透過層は128×128にresizeする。
    • 合成画像を生成する前に反射された物体は通常ガラスから遠く離れていて、反射シーン内のより大きな物体は反射合成画像においては比較的小さく見える状況を再現するため、反射層は大きな反射画像からランダムに切り取ってから128×128にサイズ変更する。
    • ただし、入力画像に大きな視差の二重反射が含まれている場合のカーネルサイズは最初の畳み込み層(conv1とconv2)と最後の畳み込み層(deconv11とdecconv12)にはより大きい9×9のカーネルが良いらしい。
  • バッチサイズ: 64
  • epoch数: 150 epoch

実装

実装はこちらに置いてます。

https://github.com/pesuchin/deep-encoder-decoder-network-reflection-removal

できるだけ論文の内容を再現しましたが、そのままだとlossが安定しないどころか大きくなりすぎて、nanやinfになったりしたので、batchnormやreluを追加したところうまくいきました。

jpeg画像に対する工夫についてはそもそもpngを混ぜていないので、まあ良いかなと実装していません。
VGG lossについては使わなくてもある程度精度出るっぽかったのと実装が面倒臭かったので使っていないです。

実験結果

学習に利用したデータセットは以下のうち、airport_insideを透過画像、artstudioを反射画像に使いました。(本当はもっと色々使うべきではある)

Indoor Scene Recognition, CVPR 09

一旦学習データに対してであれば、以下のような出力を出すようにはなりました。(左から入力、予測結果、教師)

f:id:gat-chin321:20190526101727p:plainf:id:gat-chin321:20190526101730p:plainf:id:gat-chin321:20190526101734p:plainf:id:gat-chin321:20190526101738p:plain
学習データでの出力結果

実際に試したかった冒頭の画像のうち、反射している部分だけ切り出して試してみたところ以下のような感じになりました。

f:id:gat-chin321:20190526105554p:plain
冒頭の消したかった反射の画像部分

余計にくっきり出てしまいましたね。Deep Learning先生がペンギンとか誘導灯を見るのは初めてだからだろうか…。

(追記)

lossにReflection recovery and removalのところで反射を復元するタスクも同時に解かせるようにしたところ先頭のペンギンの例でも以下の通り、比較的上手くいきました。ただ、画像の色がやや変化してしまいました。

f:id:gat-chin321:20190526225831p:plain
lossに反射を復元させるタスクを追加した場合の結果(左から入力、predict、教師、反射画像の教師、反射画像のpredict)

反射を復元させるタスクの具体的な式としては、以下の感じになります。

 {
\displaystyle
\begin{eqnarray}
L_{l2 reflection} = || Rec(F(I)) - \beta R ||_{2}^{2}
\end{eqnarray}
}

 \beta: 反射画像 Rの反射率
 R: 反射画像
 F: 特徴を抽出するための関数(特徴抽出器)
 Rec: 抽出した特徴から反射画像の復元

これを使ってlossを以下のようにして学習させました。

 {
\displaystyle
\begin{eqnarray}
L = L_{l2} + 10 * L_{l2 reflection}
\end{eqnarray}
}

10倍している理由は反射率 \betaをかけた後の反射画像を復元しようとしているので、値は基本的に小さいです。そのため、ニューラルネットとしては黒(0)にしておけばlossの値は低く抑えられるので、真っ黒(0)を出力するように学習をサボってしまうため、とりあえず10倍してサボりを容認しないようにしてみました。(スパルタ教育だ……)

学習で使った画像とかだとこんな感じになりました。以下の反射画像の教師、反射画像のpredictについてはそのままだと薄くて見にくいため、5倍してプロットしてます。

f:id:gat-chin321:20190526231357p:plainf:id:gat-chin321:20190526231400p:plain
lossを変更した後の学習中の出力(左から入力、predict、教師、反射画像の教師、反射画像のpredict)

終わりに

余談ですが、カメラのハードウェアを開発してる友人にハードウェア的に反射除去する方法があるか聞いたら「偏光フィルタでできるよ」と言われましてやる意味がない気もしましたが、偏光フィルタを忘れて撮影した後でも取り除けたり、偏光フィルタがないような貧乏人でも使えるという優位性を主張していきたい。
Twitterで偏光フィルタで検索かけると偏光フィルタ使っても反射を防げないケースも見られたので、そういうケースでも使えるんじゃないだろうか。

まあ、失敗したので、どうでもいいですね。やはりブログネタの成功確率が低い…。