複数の画像の合成処理です。カメラでの連写によって撮影された写真群を合成して1枚の写真にする方法の考察です。Pythonを使います。結果やはり一番効果絶大だったのはノイズ除去でした。Pythonこういった遊びには役立つなぁ。
拡大してません。暗がりとはいえここまでとは…。複数枚の写真をもとに、ちゃんと動体のノイズが取れてます。こんな処理です。
複数枚の写真で、びったり位置があっていれば、すなわち複数枚の画像間で同じ座標に同じものが写っていることが保証できていれば、比較的簡単な処理で合成することができ、面白い効果が出せます。
ただしこれは複数枚の画像間でびったり位置があっていることを前提にしています。こちらでいろいろ試したように、位置合わせ処理にも限界があることをある程度想定し、合成処理の方も工夫をしておいた方がよいです。
今回は位置合わせが必ずしもうまく行っていなかったり、逆にわざと位置をずらして撮影した写真の合成に関して遊んでみようと思います。
位置ずれ
こちらで試したように、特に手持ち撮影、動体被写体を対象とすると、いくら位置合わせしても複数フレーム間で確実にずれが起きます。
これらのずれを含んだまま画像を合成すると、エッジ部でぶれ、いわゆる手振れのような写真になってしまいます。
一見同じに見える2枚のプー。しかし微妙に位置ずれしています。これをプーに合わせて合成すると、
背景がブレブレです。で今度背景に位置を合わせると
プーがブレブレ。これでは面白くないです。このような位置ずれを想定した合成を考えてみます。
αブレンド
前記事でも載せましたが、基本は複数の画像間でのαブレンドです。αブレンドとは、2つの値AとBをある比率をもって加重平均を取るやり方で、式にすると、
$$ output = \alpha\times A + (1-\alpha) \times B $$
こんな単純な式です。αは0~1の範囲です。αが1に近ければAの重みが大きく、0に近ければBの重みが大きいoutputが得られます。このαを画素毎に切り替えながら合成を行って行きます。このαに一工夫入れます。
この画素毎に異なるαを持つ画像をαチャネルとか呼んだりします。OpenCVでRGBに加えてこのαチャネルを加えると、このチャネルの情報で画素の透過をしてくれたりします。
画像差分に応じたαブレンド
位置合わせを済ませてなお、2枚の画像には差分があります。まったく同じ画像でない限り少なからずあるのですが、この画素毎の差分を、位置合わせがうまく行っているか否かの評価値にして、αブレンドしてやります。
先ほどのプーを例にすると、プーに位置合わせした画像間の差分は可視化するとこんな感じ
白ほど差分が大きく、黒い箇所は差分が小さい箇所です。このように位置合わせがうまく行っている箇所では黒く、そうでない箇所は白くなります。この差分画像をそのままαチャネルとして使って、αブレンドしてやります。
差分が小さいところを合成して何がうれしいか。ですが、これでいわゆる画像のノイズが取れます。バイラテラルフィルタ的な発想です。バイラテラルフィルタとは、簡単に言ってしまうと、空間的に離れているが近い値を持つ(差分が小さい)箇所で平均化することでノイズを取り除く手法でしたが、これは時間的に離れているが近い値を持つ箇所で平均化してノイズを取り除いています。
バイラテラルフィルタに関して、もう少し詳しくはこっちにまとめました。詳しくはそちら。
効果も似てきます。
iPhoneSEで撮影した写真です。200%拡大表示です。冒頭の写真よりはマシですが、スマホの写真なんてこんなもんです。 このごつごつしたノイズが、
するっとなります。このごつごつノイズはおそらく1枚の画像からそれこそバイラテラルフィルタを強烈にかけた結果のように見えます。
言わずもがなですが、これ手です。すなわち動体です。「はいチーズ。」と言ったところで動いちゃいます。位置合わせ後の画像では、
手が動いているのを、無理やりこの手法でブロック単位に位置合わせしているので、背景との位置関係や、髪の毛との位置関係のずれで、ブロック歪が見えています。JPEGノイズではありません。が、合成時にその歪みは見えなくなっています。バイラテラル的な効果です。
コードですが、簡単です。実体は6行です。まぁPythonだから6行なのかもしれませんが、この6行を延々と説明してきただけです。
## # @brief 2枚の画像の変化が大きいところほどブレンドしないαブレンド # @param base ベース画像(numpy array) # @param frame 位置合わせした画像(numpy array) # @return 結果画像 def bai_blnd(base, frame): gbase = cv2.cvtColor(base, cv2.COLOR_RGB2GRAY).astype(np.int16) gfrme = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY).astype(np.int16) dif = np.abs(gbase - gfrme) dif = np.clip(dif, 0, 80) dif = np.stack([dif, dif, dif], 2) return (base * dif + frame * (80 - dif)) / 80
はまった所といえば唯一、引き算した結果が符号付になるので、あらかじめ符号付整数にキャスト( .astype(np.int16) )しておく必要があったくらいです。わからなかったぁ。
グレースケールにしてから、差分(dif)を取って、そのdifを3chに展開(np.stack)してこれをαブレンドしているだけです。この際、直値で使っている80はパラメータです。経験的に出した数字です。意味はないです。差分をダイレクトに使うのではなくクリップして使っています。
この関数の仕様としては、差分が大きいほどbaseの結果が、小さいほどframeの結果が出ます。この関数の返り値として出てきた画像を複数加算していけば、上記のノイズ除去済みの画像が作れます。
ノイズバリバリで読めなくなった文字も少し回復する例もありました。
ノイズ除去の効果はそれなりにあるかと思います。
応用編1 流し撮り風合成
位置合わせがうまく行っていない箇所の差分が大きいことに着目して、少し応用をしてみます。いわゆる流し撮りを模します。詳細はこちらで試しました。
流して撮りたい被写体で位置合わせをして、位置がずれた背景部をわざと移動方向にぼかして、やはりその差分情報をαチャネルとしてαブレンドする。といったやり方です。
この例だと、位置が合った電車の先頭車両はぼけず、位置が合っていない背景部は差分が大きく発生し、ぼける。といった仕組みです。
コードも先ほどのコードとほとんど変わらないですね。位置合わせ済みのbaseとframeの2枚の画像差分に応じて、frameと、それをぼかしたblurを合成する。といったやり方です。
## # @brief ぼかし(移動) # @param img 入力画像(numpy array) # @param size カーネルサイズ(大きいほどぼける) # @param angle 移動角度 # @details 角度向きにカーネルサイズ分の平均化を行う。 # @return 結果画像 def blur_image(img, size, angle): hsize = size//2 kernel = np.zeros((size, size), np.uint8) line = np.full((1, size), 255, np.uint8) kernel[hsize:hsize+1, :] = line affine = cv2.getRotationMatrix2D((hsize, hsize), angle, 1.0) kernel = cv2.warpAffine(kernel, affine, (size, size), flags=cv2.INTER_NEAREST) kernel = kernel/np.sum(kernel) return cv2.filter2D(img, -1, kernel) ## # @brief 変化が大きいところほどぼかす # @param base ベース画像(numpy array) # @param frame 位置合わせした画像(numpy array) # @param size カーネルサイズ(大きいほどぼける) # @param angle 移動角度 # @return 結果画像 def blur_blnd(base, frame, size, angle): blur = blur_image(frame, size, angle) gbase = cv2.cvtColor(base, cv2.COLOR_RGB2GRAY).astype(np.int16) gfrme = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY).astype(np.int16) dif = np.abs(gbase - gfrme) dif = np.clip(dif, 0, 40) dif = np.stack([dif, dif, dif], 2) return (blur * dif + frame * (40 - dif)) / 40
応用編2 ポートレート風合成
位置合わせができている箇所にあたかもフォーカスがあって、背景がぼけて見えるような、そんな効果を出してみました。これも基本は同じ。位置合わせがうまく行っていない箇所の差分が大きいことに着目して、わざと背景を位置合わせできないようにしてやります。
被写体と背景がそれなりに離れている位置関係で、カメラの方をずらすと、背景と被写体のずれ量が変わります。冒頭のプーがそれです。なのでプーに合わせると背景がずれ、背景に合わせるとプーがずれます。これを応用します。
正直コードとしては先の流し撮りのものとほとんど変わりません。ぼかしの部分が少し違うだけで、応用と言いながらさほどひねりが効いたものではありません。
## # @brief 差分に応じたぼかし # @param base 入力画像(numpy array) # @param frame 入力画像(numpy array) # @param size カーネルサイズ(大きいほどぼける) # @return 結果画像 def blur_from_diff(base, frame, size): img1 = base[:,:,1]/255 img2 = frame[:,:,1]/255 diff = np.clip(np.abs(img1 - img2)*5, 0, 1) diff = np.stack([diff, diff, diff], 2) img1 = base/255 kernel = np.ones((size, size), np.float64) kernel = kernel/np.sum(kernel) blur = diff*img1 blur = cv2.filter2D(blur, -1, kernel) diff_sum = cv2.filter2D(1-diff, -1, kernel) blur = blur + (diff_sum * img1) return blur*255
ぼかしの処理がよく意味が分からないと思います。なぜこんな意味不明なぼかしをしているのかは元記事を見ていただければと思います。一応意味があります。
まとめ
位置合わせがあと一歩及ばずな画像間のずれを、その差分画像をαチャネルとして用いることで、解消しつつ、複数枚合成を行う手法に関していろいろ遊んでみました。これまでの記事の焼き増しですが、まとめの切り口としてはいいかな。すっきりしました。
ちなみに失われたEXIF情報は、こちらで復活させます。
前回が位置ずれ無しを想定した合成なので、そこで得られるような効果は今回のやり方では出せません。星とか。流れとか。ただノイズ除去に関しては強力です。正直これまで暗がりでスマホ撮影する気にまったくなれませんでしたが、後処理でここまで回復するならとりあえず撮っとくかな気分になります。それをソースにレタッチする気にもなります。
きっとPhotoshopでも同じことできるんだろうなぁ。Pythonの方が楽な気がするけど。
フレーム間の差分画像をαチャネルとして活用する例はこれだけじゃないかもしれません。アイディア次第で他にもアプリケーションは考えられるかもしれない。
コメント