takuti.me ABOUT

2013-12-08

Poisson Image Editingでいい感じの画像合成ができるやつを作る on Web

Aizu Advent Calendar 2013 8日目の記事です。

デモ

まずは作ったやつ(デモ版)からどうぞ。
Poisson Image Blending - Demo

Step1はいじらなくていいので、Step2で適当にマスク領域を塗ってあげてください。
Screen Shot 2013-12-08 at 12.41.01 AM
こんな感じで塗れたら、HEREボタンをクリック。するとStep3にマスクをかけた領域だけ乗っかります。
Screen Shot 2013-12-08 at 12.43.06 AM
そうしたら矢印ボタンで位置を調整して、
Screen Shot 2013-12-08 at 12.44.40 AM
「ここで合成だー」と思ったところでOKボタンをクリックすれば、
Screen Shot 2013-12-08 at 12.44.51 AM
真顔モナリザの完成です。

このようにいい感じの画像合成ができる手法は、ググればC++やPython、さらにHTML5 Canvasでの実装もすでに存在します。ま、まぁマスク領域自分で塗れるようにしたから新規性あるよね・・・。

アプリ版

デモを利用して、合成したい2枚の画像を自分で選べるものを作りました。アプリ版です。
Poisson Image Blending - App

まずはベース画像(合成先)ソース画像(切り抜いて合成する方)をそれぞれ選択します。画像サイズはいずれも150ピクセル×150ピクセルに限定しています。それより大きい/小さい画像を選択すると縮小/拡大されます。

ちゃんと両方選択できると、Start Appボタンが有効になるのでクリック。
Screen Shot 2013-12-08 at 7.26.53 AM
すると先ほどのデモ版と同様の画面が表示されるので、Step2でマスク領域を塗って、Step3で位置調整、合成という流れで遊んでください。
blend_result
こんな感じになります。アゴ

実際に顔写真でやってみるのが個人的には一番おもしろいと思います。

あ、フッターみたいな変な所に結構重要なボタンがあります。最悪ですね。
Screen Shot 2013-12-08 at 2.23.06 AM

Importing gradients と Mixing gradients

ここで無視していたStep1の話をしましょう。

いい感じの画像合成では画像の勾配(Gradients)が大切です。画像における勾配とは、隣り合っているピクセル同士でRGB値がどれだけ違うかということ。
blend_result
先ほどの合成結果を見ると、色はベース画像に馴染みつつも、どこに線があるかという情報はソース画像のものを受け継いでいます。このバランスがいい感じの合成を実現しているんですね。

この線の情報(=合成結果の勾配)の求め方は2通りあります。それがImporting gradientsMixing gradientsです。

Importing gradientsは先ほども例に挙げたように、ソース画像の勾配をそのまま合成結果に利用します。一方でMixing gradientsは、各ピクセルに対してベース画像とソース画像の勾配を比較して大きい方を採用するというものです。

これを切り替えて試せるのが、デモ版にもアプリ版にもあるStep1のラジオボタンです。さらにデモ版ではFacesHand&Signという2種類のベース画像・ソース画像の組み合わせを切り替え可能にしました。勾配の取り方2種類と画像セット2種類なので計4通りの合成を試すことができ、結果は以下のようになります。

Importing Mixing
Faces faces_importing faces_mixing
Hand&Sign hand_importing hand_mixing

見ての通り、Facesの場合はImportingのほうが期待通りの結果になっています。一方Hand&Signでは、Mixingのほうが手のシワを残しつつ文字を合成していて綺麗な結果になっています。文字上では手のシワよりも文字の勾配のほうが大きく、その他の部分は変化の少ない白い紙の写真なので手のシワの勾配のほうが大きかったというわけです。

ImportingとMixingのどちらが良いのかは一概に言うことはできず、合成する画像の組み合わせによって勾配の取り方も適切な方を使う必要があるんですね。

Poisson Image Editing

さて、そろそろ今回の画像合成手法をちゃんと紹介しましょう。
(トップダウン式な記事で我ながらすばらですね)

今回利用している手法はPoisson Image Editingというタイトルの、「ポアソン方程式を解くことで画像補間をいい感じにできるよ」という内容の論文で提案されているものです。PDFは以下より。

P. Pérez, M. Gangnet, A. Blake. Poisson Image Editing. ACM Transactions on Graphics (SIGGRAPH'03), 22(3):313-318, 2003.

いい感じの画像補完の具体例が今回実装したシームレスな画像合成で、これは論文の2章と3章にあたります。式の導出から書こうと思いましたがイマイチ自身無いのと実装で力尽きたのとでやめました。気になった方は原文読んで下さい。

結論だけ書くと、論文内の式(7)を $f_p=$ の形に変形して解けば終わりです。すなわち、すべての $p \in \Omega$ に対して以下の式が成り立つような連立一次方程式を解くと言う問題。

$$ f_p = \frac{\sum_{q \in N_p \cap \Omega}f_q + \sum_{q \in N_p \cap \partial \Omega}f^*_q + \sum_{q \in N_p}v_{pq}}{|N_p|} $$

この式の意味するところは論文中の図1なんかを参照しながらイメージするしかありません。無理やり言葉で説明するとだいたい以下のような雰囲気です。

ベース画像=合成先ソース画像=切り抜いて合成する方です。

$\Omega$
合成結果の中で、切り抜いたソース画像が合成された領域
$f_p$
ある点 $p \in \Omega$ での $f$ の値で、これを求めることが今回の目標
$f$ は $\Omega$ 内の各ピクセルのRGB値を示す関数
$f^*_p$
ある点 $p$ での $f^{\*}$ の値
$f^{\*}$ はベース画像内の各ピクセルのRGB値を示す関数(既知)
$N_p$
ある点 $p$ の近傍点の集合(今回はその点の上下左右、4近傍)
$\partial \Omega$
$\Omega$ の境界領域
$p \notin \Omega$ を満たす点のうち、近傍点が1つでも領域 $\Omega$ に入っていれば $p \in \partial \Omega$
自分自身は入っていないけど近傍点のどれかは入っているという状態
$g_p$
ソース画像からマスクをかけて切り抜いた部分の中にある点 $p$ でのRGB値
$v_{pq}$
ある点 $p$ とその近傍点のうちの1つ $q$ の間の勾配
Importing $g_p-g_q$
Mixing ベース画像の勾配の方が大きければ $f^\*_p - f^\*_q$, そうじゃなければ $g_p-g_q$

これに沿って実際に合成を行っている(=$f_p$を求めている)コードは、ざっくりと流れを追うと以下のような感じ。

do {
  // 全ピクセルを見る 今回は簡単のため画像の端のピクセルは考慮しない
  for(var y=1; y<base_size.height-1; y++) {
    for(var x=1; x<base_size.width-1; x++) {
      if(/* もしStep2で塗ったマスク領域の中なら合成後のRGB値を推定 */) {

        // そのピクセルのRGB各色について連立一次方程式を解く
        for(var rgb=0; rgb<3; rgb++) {
          var sum_fq = 0;       // (1)
          var sum_boundary = 0; // (2)
          var sum_vpq = 0;      // (3)

          // 近傍点(4点)それぞれについて
          for(var i=0; i<num_neighbors; i++) {

            if(/* もし近傍点がStep2で塗ったマスク領域の中なら */) {
              // (1)の加算
            } else { // 近傍点は境界領域の中
              // (2)の加算
            }

            if(/* Mixingで、ベース画像の勾配の方が大きければ */) {
              // (3)の加算(ベース画像の勾配)
            } else {
              // (3)の加算(ソース画像の勾配)
            }
          }
          // そのピクセルのRGB推定値の格納
        }
      }
    }
  }
  if(/* 全ピクセルの推定が終わったので収束判定 */) break;
} while(true);

takuti / poisson-image-blending

(1)〜(3)は先に示した $f_p = $ の式の右辺、分子の3つ項に対応します。

(1)
$ \sum_{q \in N_p \cap \Omega}f_q $
(2)
$ \sum_{q \in N_p \cap \partial \Omega}f^*_q $
(3)
$ \sum\_{q \in N\_p}v\_{pq} $

今、すべての $p \in \Omega$ に対して $f_p$ を求めているため、連立一次方程式の未知数(=合成される領域内の点の数)は $|\Omega|$ です。$|\Omega|$ 個の未知の点を順番に推定していくことになりますが、推定途中でも(1)では $f$ の値を利用しています。

このような、連立一次方程式の解の推定途中で推定済みの値とまだ推定されていない(過去の)値の両方を計算に利用する形は、ガウスサイデル法の漸化式そのままです。そこで上記簡易コードではガウスサイデル法による解の推定を行っており、推定値の収束を合成の終了としています。

合成の軸になる処理はこのようなシンプルな数値計算で完結します。しかし実際はCanvasの操作なんかでコードの肥大化が深刻。

まとめ

というわけで、Poisson Image Editingという論文で提案されたシームレスな画像合成をCanvasとJavaScriptで実装してみて、おまけにアプリ版も作ってみたお話でした。

実は以前この手法の画像合成を試したことがあったのですが、なぜかうまくいかず詰んだので放置していました。しかし再挑戦したらなんとかなった。これは今期履修している数値解析のおかげかな!?(申し訳程度の会津大要素)

アプリ版はまた気が向いた頃に実装の見直しや改善をするかもしれません。

とりあえず今はこの記事を書き上げたことでようやくBDFS始められるので僕は消えます。

明日、Aizu Advent Calendar 2013 9日目の担当は95さんです!しゃす!

影で就活Advent Calendarやってる人がいるのでよかったらそちらも見てあげて下さい。

参考