Python初心者のための目的志向のプログラミング。前回記述した、連番JPEGからのフェード付きタイムラプス動画作成コードを一部編集し、画角のパンとズーム(ついでに回転)を加えて、もう少しもっともらしい動画に仕立てます。
今回の記述をしてたら、機能は増えたのに行数は逆に減ってしまいました…。
やりたいこと
適当に設置したカメラでインターバル撮影した複数のJPEGから、タイムラプス動画を作る。適当に設置したので、画角はずれており、残したい画像が必ずしも中心にないため、画像のトリミングを行う。また、三脚が傾いていたため、その補正(回転)も行う。(キャンプ場で設置時フレーミングしないです。ズームも未調整で設置をした前提の画像)開始に真っ黒からのフェードイン。終了に真っ黒へのフェードアウト。
(ここまで前回完成)
開始のフレームトリミング位置と終了のフレームトリミング位置の間を徐々に移動させる。(パンとズームさせる。)上の図の例は、左上から右下に向かってパンしながらズーム。
方針
開始座標と終了座標を指定してその間をフレーム番号をもとに線形補間で求める。を加える。
コード
肝心のコード。for文の中の記述が多くなってしまったので、さすがに可読性重視の方針に切り替えた。(処理時間は伸びるだろうなぁ)
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] -= shift[0] trans[1, 2] -= shift[1] return cv2.warpAffine(img, trans, (WIDTH, HEIGHT), cv2.INTER_CUBIC) ############################### def main(): #ファイル取得 files = os.listdir(ROOT_PATH) start = np.array([ 0, 0,0,1.0]) #座標x,座標y,回転角,倍率の順 end = np.array([400,200,0,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): img = cv2.imread(ROOT_PATH + "\\" + file) print(ROOT_PATH + "\\" + file) current = (end*i + start*(len(files)-i)) / len(files) #線形補間 frame = convert(img,current[:2],current[2],current[3]) alpha = 1 if i < FADE: alpha = i/FADE elif i > len(files)-FADE: alpha = (len(files)-i-1)/FADE blended = cv2.addWeighted(frame, alpha, mask, 1 - alpha, 0) video.write(blended) video.release() if __name__ == '__main__': main()
解説
追加コードに対して説明します。以前のコードに対しての差分として大きいのが、for文の分解辞めました。3つのfor文にほぼ同じ記述が登場し、メンテナンス性が悪くなったので。
あと座標と角度、倍率の指定方法も変更しました。線形補間を1行で書きたかったので。
start = np.array([ 0, 0,0,1.0]) #座標x,座標y,回転角,倍率の順 end = np.array([400,200,0,1.0])
np.arrayで、トリミング開始のx座標、開始のy座標、回転角度、倍率の4つをいっぺんに指定する形式にしました。理由は後で説明します。
以前
shift = [100,100] angle = 45 scale = 0.5
が切り出し開始の座標(x,y)(shift)、回転角度(angle)、倍率(scale)を設定していた箇所。そのshiftに相当する箇所をstart[0],start[1]へ、angleをstart[2]、scaleをstart[3]へマッピングしただけです。
次に
current = (endi + start(len(files)-i)) / len(files) #線形補間
ですが、ここで現在処理中のフレームに対応したトリミングのパラメータを
計算しています。開始と終了の値、現在のフレーム番号と総フレーム長から線形補間をしています。同一インデックスの、配列の中身同士の計算を一発でしてくれるので、今回はすべてのパラメータを1つの配列にマッピングさせました。これが変更の理由です。
list形式だと一発計算ができなかったので、np.array型。for文書かなくてよいのですごく楽ちん。Cだと
for(int j=0;j<4;j++) current[j] = (end[j]i + start[j](len(files)-i)) / len(files)
ってところですかね。
続いて、このパラメータを使ってトリミング
frame = convert(img,current[:2],current[2],current[3])
convert関数の仕様は前回から変えていないので、配列の要素を渡しています。まったく変えてないわけではなく、こっそり
trans[1, 2] -= int(shift[1])
の部分。intへのキャストはやめました。shiftがnumpy arrayであることを保証できるので、キャストしなくても大丈夫なのと、補間で小数点以下もでるのでキャストしちゃもったいなかったので。
次が少し速度劣化の要因
alpha = 1 if i < FADE: alpha = i/FADE elif i > len(files)-FADE: alpha = (len(files)-i-1)/FADE
for文の中で毎回ifを評価しているし、アルファブレンドする必要がないフレームでもaddWeightedが呼ばれてしまうので、とても嫌なのですが、可読性、メンテナンス性をとりました。
やってることは前のコードと同じです。開始からFADEまでのフレームではフェードイン。終了のFADE前から終了までの間フェードアウトさせるためのalphaブレンドの値をif/elifの中で計算しています。alpha=1であればframeから何も変化しません。(addWeightedの中で if alpha==1 then returnが効いてればいいのですが…)ここがボトルネックだとするとelseでただの代入を入れたいところです。
ただ数フレームの動画だと速度劣化は体感できませんでした。よっぽどmp4へのエンコードの方が時間かかりそうだし、埋もれるのかな。
まとめ
とりあえずパン、ズームも加えた動画が作れました。
前回の記述から、for文が、3つに分かれてしまった部分は是正しましたが、速度劣化が気になります。トータル60行に減りました。すげーなpython。確かにこれならちょっとした数値解析等は簡単に記述できそうです。片鱗が見れた気がします。勉強になります。
今回のコードですが、動画のクオリティをつかさどるパラメータが、サイズとフレームレートしかありませんが、適当に撮った連番JPEGがそれなりに見れたものになるので、まずまずかなと思います。(カメラ内で作ったそれよりはだいぶまし)
いったんここまでで、キャンプ・タイムラプス動画作成までの手順は踏めました。動画の連結や、音楽の挿入、キャプション等凝った動画を考え出したらきりないので、ここまで。この機能で60行というシンプルさが、この場合価値かと思うので。
とはいえ、最後にちょっとだけ機能追加して終わりにしようかなぁ。
コメント