Python初心者のための目的志向のプログラミング。前回記述した、連番JPEGからのシンプルなタイムラプス動画作成コードを一部編集し、開始と終了フレームに対してフェードイン、フェードアウトを加えて、もう少しもっともらしい動画に仕立てます。
これも結論数行の追加でできました。前回のコードに20行程度加えるだけで完成してしまいました。
やりたいこと
適当に設置したカメラでインターバル撮影した複数のJPEGから、タイムラプス動画を作る。適当に設置したので、画角はずれており、残したい画像が必ずしも中心にないため、画像のトリミングを行う。また、三脚が傾いていたため、その補正(回転)も行う。(キャンプ場で設置時フレーミングしないです。ズームも未調整で設置をした前提の画像)
(ここまで前回完成)
開始に真っ黒からのフェードイン。終了に真っ黒へのフェードアウト。を加えます。
方針
真っ黒フレーム(画像)を用意して、それに対してフレーム番号をもとに、アルファブレンド(フェード)していく。
コード
前回のコードとほとんど変わっていません。間違い探しレベル。
import os import cv2 import numpy as np ROOT_PATH = 'E:\\tmp\\photo4' #ソース画像が置いてあるパス SAVE_FILE = 'E:\\tmp\\photo4.mp4' #出力動画 WIDTH = 1920 #動画出力幅 HEIGHT = 1080 #動画入力幅 FPS = 10.0 #フレームレート FADE = 5 #フェードイン・フェードアウトフレーム数 ############################### # imgをrotateだけ回転しshiftだけずらす def convert(img, shift,angle,scale): # img ソース画像 # shift シフト量(shift[0] x方向,shift[1] y方向) # rotate 回転角度(度) height, width = img.shape[:2] center = (int(width / 2), int(height / 2)) trans = cv2.getRotationMatrix2D(center, angle, scale) trans[0, 2] -= int(shift[0]) trans[1, 2] -= int(shift[1]) return cv2.warpAffine(img, trans, (WIDTH, HEIGHT), cv2.INTER_CUBIC) ############################### def main(): #ファイル取得 files = os.listdir(ROOT_PATH) shift = [-39,-178] angle = -2 scale = 1.0 # 画像入力 fourcc = cv2.VideoWriter_fourcc('m','p','4','v') video = cv2.VideoWriter(SAVE_FILE, fourcc, FPS, (WIDTH, HEIGHT)) mask = np.zeros((HEIGHT,WIDTH,3), dtype="uint8") #フェードするフレーム(真っ黒を想定) for i,file in enumerate(files[:FADE]): img = cv2.imread(ROOT_PATH + "\\" + file) print("FADE IN:" + ROOT_PATH + "\\" + file) frame = convert(img,shift,angle,scale) alpha = i/FADE blended = cv2.addWeighted(frame, alpha, mask, 1 - alpha, 0) video.write(blended) for file in files[FADE:-FADE]: img = cv2.imread(ROOT_PATH + "\\" + file) print(ROOT_PATH + "\\" + file) frame = convert(img,shift,angle,scale) video.write(frame) for i,file in enumerate(files[-FADE:]): img = cv2.imread(ROOT_PATH + "\\" + file) print("FADE: OUT" + ROOT_PATH + "\\" + file) frame = convert(img,shift,angle,scale) alpha = 1-(i+1)/FADE blended = cv2.addWeighted(frame, alpha, mask, 1 - alpha, 0) video.write(blended) video.release() if __name__ == '__main__': main()
解説
追加コードに対して説明します。
mask = np.zeros((HEIGHT,WIDTH,3), dtype="uint8") #フェードするフレーム(真っ黒を想定)
で出力サイズと同じだけのRGB 3chを想定した真っ黒(画素値全部0)の画像(mask)を生成しています。zerosでゼロ埋めされます。グレースケールの画像が入ってきたら残念ながら駄目でしょうね。白に向かってフェードさせたければ、
mask = np.full((HEIGHT,WIDTH,3),255, dtype="uint8") #フェードするフレーム(真っ白を想定)
としてあげればよさそうです。この255の部分を任意の値にすればグレーへのフェードもできます。
続いてfor文。3つのフェーズに分けています。フェードイン、通常、フェードアウト定数FADEでループを区切っています。
まずフェードイン
for i,file in enumerate(files[:FADE]):
リストの区間の指定が、いかにもpythonらしい記述。まだ慣れません。
続いて通常の箇所
for file in files[FADE:-FADE]:
普通です。FADEから最後-FADEまでのループ
そしてフェードアウト
for i,file in enumerate(files[-FADE:]):
このマイナスの区間指定も慣れない。
エラー処理はしていません。全フレーム数はFADEの2倍より多くある前提です。またFADEは1より大きい正の整数を前提としています。これも性善説に立っています。
ここで見慣れないenumerateという関数ですが、これこういう時にはすごく便利で、indexも同時に返してくれます。なので、
for 変数1,変数2, in enumerate(list)
とすると変数1にindexが、変数2にlistの中身が返ってきます。今回fade in/outの時にindex番号が必要なので、別途カウンタを回すよりクールに書けてます。(知らなきゃこんな書き方できないな…)
まずフェードイン。これはindex iを使って、現在の画像frameと、黒を示すmaskとのアルファブレンド比率alphaを求めています。その後addWeighted関数でアルファブレンドしているだけです。これはOpenCVの関数。iが小さいほどmaskの比率が高いことになります。
alpha = i/FADE blended = cv2.addWeighted(frame, alpha, mask, 1 - alpha, 0)
ここでframeにあたる画像の作り方ですが、これは前回の解説にあるように、オリジナルの画像から切り出して変換するだけで、コードの変更は加えていません。
同様にフェードアウト。これはフェードインの逆をやっているだけです。アルファブレンド比率alphaの計算ですが、1から先ほどの計算結果を引く形で逆転させています。フレームが進むほどmaskの比率が高くなります。(i+1してるのは最後真っ黒で終わるため)
alpha = 1-(i+1)/FADE blended = cv2.addWeighted(frame, alpha, mask, 1 - alpha, 0)
enumerate関数の仕様でしょう。リストの途中(-FADE)からループを回していますが、iは0から始まっています。
これだけです。FADEで指定したフレーム数だけフェードイン、フェードアウトします。それ以外のフレームに対しての処理は以前のコードのままです。
まとめ
とりあえず簡単なコードの追加でフェードイン、フェードアウトする動画が作れました。記述量を少なくして、わかりやすくすることを優先させたので、エラー処理や、イン・アウトのフレーム数を同じにするなど、一部手抜コードになっていますが、さほど困りません。(残念なのはFADEを0にしてもフェードなくした動画にはならない…)
同じような記述のfor文が、3つに分かれてしまったのが少しダサいですが、これよりスマートな書き方が思いつきませんでした。処理時間考慮しなければfor文にifをネストさせるんだろうなぁ。このあたりが処理速度気にしたCの癖なのかな。がトータル70行です。まぁいいか。
黒からのフェードインを入れると動画のサムネイル表示が黒くなってしまって、どんな動画だかわからない問題があったりするので、その場合はフェードインだけは捨ててもいいかもしれません。するとさらに行数は減ります。
次は画角のパン、ズームのエフェクトを入れようと思います。
コメント