前回の記事で複数の写真の位置合わせを行う処理に関していろいろ試してみました。その結果ぼちぼち位置合わせができて、画像間で座標位置が合うようになったので、その座標があった画像群を合成して、各種効果を得てみようと思います。位置合わせと合成。この2つの処理でいろいろ遊べます。今回はその合成処理のパートです。
北極星で位置合わせ。したわけではないですが、夜空の連続撮影からこんな感じの結果が得られます。(これはカメラ固定での撮影です)これをPythonで試します。
以前記事にしましたが、複数枚の画像の座標が合うことでいろいろなエフェクトを画像に加えることができます。
ここでもまとめましたが、わかりやすいのはノイズ除去。あとは長秒露光を模した各種効果もいろいろできます。これらの効果を出すための画像合成処理に関してまとめてみようと思います。
今回は複数枚の画像間で位置合わせばばっちり決まって、被写体も動きの無いもの、ないし動きがあってもそれをぼかして効果を出すものを対象にします。動きがあるものをぼかさず、静止させて合成する処理に関しては、長くなったのでまた別記事にします。まずは基本のきです。
合成処理
一言で合成と言ってもいろいろな意味を持っていますが、今回の合成は複数画像間の同一座標の画素値同士の演算の事とします。
いわゆるHDRみたいな処理もこの位置合わせからの合成が基本になっています。
複数の画素、ないしレイヤーの合成はPhotoshopでもいろいろな演算が用意されています。わかりやすいのは、加算、減算、乗算あたりでしょうか。画素値同士を足したり引いたりします。凝ったものだと比較(明/暗)、差の絶対値とかそのあたりまでは直感でなんとなく処理が想像できます。
今回の場合は、同一座標の画素間の画素値の演算を行い、新たな画素値を作る処理です。
αブレンド
定番はαブレンドでしょう。今回の合成も基本的には一工夫したαブレンドになっています。
αブレンドとは、2つの値AとBをある比率をもって加重平均を取るやり方で、式にすると、
$$ output = \alpha\times A + (1-\alpha) \times B $$
こんな単純な式です。αは0~1の範囲です。αが1に近ければAの重みが大きく、0に近ければBの重みが大きいoutputが得られます。このαを画素毎に切り替えながら合成を行って行きます。(今回はすべて1:1のαブレンドの例ばかりですが…。)
合成の基本はαブレンドです。
加算平均
位置合わせで画素がびったり合った場合には、単純に足して2で割る平均処理が使えます。三脚固定の連写画像などがこのソースとして想定されます。これにより、ノイズが乗った2枚の画像からノイズ成分が幾分取り除けます。
座標がびったり合っていれば、画像がぼけることはありません。逆に位置があっていないとエッジ部でボケが起こります。いわゆる手振れ写真のようになります。
N枚の写真がソースであればN回足して、Nで割ればよいだけです。単純です。これにより、静止物であればノイズが取り除けます。
これが、
こんな感じ。ノイズが取れてます。1枚の画像からノイズを頑張って押さえている様子がわかりますが、複数枚合わせてやると効果絶大です。デジカメやスマホでも、マルチショットノイズ除去とかそんな名前で、こんな機能があったりしますが、基本はこんな処理だと思います。足して割るので、1:1でαブレンドしたのと同じ意味ですね。
街並みを定点撮影したものであれば人が消えたような写真になります。滝や雲の流れとか、そんなものを撮影しても面白いです。
こんな写真を複数連写して加算合成すると、
こんな感じ。単純な加算合成でも位置さえ合えば面白い効果が出ます。
ソースコードはホント、単純に足してNで割るだけ。面白くもなんともないですが、これだけの記述でこの効果が出せるのは面白いと思います。
import cv2 import glob flist = glob.glob("src/*.jpg") #ソースの写真群がある場所 sums = cv2.imread(flist.pop()).astype(np.uint32) for fname in flist: sums += cv2.imread(fname) cv2.imwrite(dstName, (sums/(len(flist)+1))) #平均化
比較(明)合成
少し凝った合成です。これは複数の画像の明るいところだけを選んで合成するやり方です。どんな効果が期待できるかというと、星の軌跡を写したり、車のヘッドライトやテールランプの軌跡を残したりとかそんな合成に使えます。星の軌跡では定番みたいです。暗所での、いわゆるバルブ撮影とか、長秒露光とかそんな撮影の仕方を模したものです。
こんな画像をソースにします。スマホオート撮影です。これも位置がびったり合っている前提で、複数の画像の一番明るい画素を選んで合成すると、
こんな感じになります。コードもやはり単純で、
import cv2 import glob flist = glob.glob("src/*.jpg") #ソースの写真群がある場所 sums = cv2.imread(flist.pop()) for fname in flist: current = cv2.imread(fname) sums = np.maximum(current,sums) # ここで比較明合成! cv2.imwrite(dstName, sums)
これだけです。np.maximumがいい感じに値の大きいものだけ選んでくれるので、それを複数回繰り返すだけです。
さすがにスマホソースではないですが、三脚固定でこんな感じの北斗七星をインターバル撮影したものをこのやり方で合成すると、
うーん、インターバルを5minもとってしまったので破線になってしまいました。が北極星が動かないことは確認できます。もう少し短いインターバルがよいですね。
処理より被写体や撮影センスが問われます…。
疑似アンダーによる加算合成
星空とかはこんなかんじでもいいかもしれませんが、この車のライトが被写体だと少し明るすぎます。1枚1枚の写真が適正露出すぎます。オートなので当たり前です。実際は長秒露光をして撮影することを模すので、合成する写真を少しアンダーで撮った写真にしてやります。が、適正露出で撮ってしまったものはしょうがないので、処理でアンダー露出にして加算合成します。
夜間の光を撮影した適正露出写真を、アンダー露出へ変換するのはいわゆるトーン調整をして中間調部を暗くしてやります。トーンカーブとしては、極端ですが
こんなカーブを当てましたが、この式に何の根拠もありません。きれいなトーンカーブを関数で作りたかっただけです。入力を8bitレンジ、出力を12bitレンジにしています。光の部分は本来これだけ明るかっただろうことを仮定しています。
$$ out = in^{8.8} $$
こんな式です。いわゆるガンマの式ですが、指数を思いきり大きくしてやりました。また複数枚、毎画素このべき乗の計算をしてられないので、あらかじめ計算済みのLUTを作ってそれを適用する形にしました。最後逆ガンマで戻します。
import cv2 import glob flist = glob.glob("src/*.jpg") #ソースの写真群がある場所 # トーンカーブ(LUT)作成 lut = np.arange(0, 256) lut = 255 * ((lut / 255) ** 2.2) # 8bit 逆ガンマ作成(浮動小数点) lut2 = 4095 * ((lut / 255) ** 8.8) # 12bit アンダー露出ガンマ(浮動小数点) lut = np.maximum(lut, lut2) # 大きい方採用(ダーク部がつぶれるので) sums = lut[cv2.imread(flist.pop())] for fname in flist: current = cv2.imread(fname) sums += lut[current] # ここで加算合成! out = sums/(len(flist)+1) # 平均化 out = np.clip(out, 0, 255) # 8bitへクリップ out = (255 * ((out / 255) ** (1 / 2.2))).astype(np.uint8) cv2.imwrite(dstName, out)
これだと、この写真群の場合少しマシになって
こんな感じになりました。
まとめ
コードの短さから分かるように、比較的単純な処理で、位置合わせ済みの連写写真から面白い効果が出せます。
くどいですが、すべて位置合わせがびったり決まった写真群が対象の処理です。先の位置合わせの記事でも書きましたが、静止物であればまぁぼちぼち位置合わせできます。また三脚使えばそりゃうまく行きます。が、手持ち、動体だとこうはいきません。
次にこの微小位置ずれが起こってなおうまく合成できるような工夫を考えてみます。
おまけ(EXIF付与)
あとこういった写真合成をすると、個人的に気になるのが保存画像へのEXIF情報の欠落です。これに関しては悔しいので、最終画像からEXIFを取り出しておいて、保存時にくっつける。といった方法を取っています。OpenCVでやり方が見つからなかったので、基本はPillowを使って取り出しています。
from PIL import Image import numpy as np image = Image.open(fname) exif = image.info["exif"] # exif情報取得 base = np.array(image, dtype=np.uint8) out = 処理(base) Image.fromarray(out).save(dstName, "JPEG", exif=exif, quality=95)
こんな感じでEXIF付与して満足してます。OpenCVとPillowの読み取りには、仕様差の罠もあるので気を付けてください。
コメント
こんにちは。
画像合成 のコード、参考にしようと試してみましたが、恐らく以下のところが原因で、エラーが発出されます。
for fname in flist:
sums += cv2.imread(fname)
cv2.imwrite(dstName, (sums/(len(flist)+1))) #平均化
エラー
OpenCV(4.5.1) C:\Users\appveyor\AppData\Local\Temp\1\pip-req-build-kh7iq4w7\opencv\modules\imgcodecs\src\loadsave.cpp:682: error: (-2:Unspecified error) could not find a writer for the specified extension in function ‘cv::imwrite_’
cv2.imwrite()の()内の指示内容が理解できていません。
私の認識では、第1引数に保存ファイル名、第2引数が画像 です。
どうあるべきか、教えていただけましたら幸いです。
エラーの内容からどうも第一引数の文字列(拡張子)の方に問題がありそうに見えますが…。dstNameのところに”out.jpg”とかそんなので解決しませんかね?