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行です。まぁいいか。
黒からのフェードインを入れると動画のサムネイル表示が黒くなってしまって、どんな動画だかわからない問題があったりするので、その場合はフェードインだけは捨ててもいいかもしれません。するとさらに行数は減ります。
次は画角のパン、ズームのエフェクトを入れようと思います。
コメント