まずはこちらをご覧いただきたい
※ ちょっとだけ音が出ます。
お分かりいただけただろうか
これはタイム ディスプレイスメント(Time Displacement)と呼ばれるエフェクトです。
原理は意外に単純です。縦×横のサイズが1920×1080ピクセルの動画のうち、ある$i$番目のフレームにこのエフェクトをかけた結果の図を以下に示しています。フレームは水平方向に$N$個のセグメントに分割されています。セグメント$0$は現在の$i$番目のフレームを、セグメント$N$は$N-1$フレーム前のセグメントを表示しています。
同じフレームの中で上から順に1フレームずつ遅延させた画像を表示するため、これが動画のすべてのフレームに適用されると、動いている人物や物がうねうねと波打ったように変形して見えます。
タイムディスプレイスメントの使い道
飲み会で失笑をかう以外に使い所はないかもしれないです。効果的に使えばスタイリッシュな映像になりますが、そうでなければただただ気持ち悪いです。効果的な例はこちら → https://vimeo.com/185412081?fl=pl&fe=cm
タイムディスプレイスメントの実装
では実装してみます。
import cv2
cap = cv2.VideoCapture("path/to/video.mp4")
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(cap.get(cv2.CAP_PROP_FPS))
_, first_frame = cap.read()
writer = cv2.VideoWriter(
"path/to/output/video.mp4",
cv2.VideoWriter_fourcc(*"MJPG"),
fps,
(width, height)
)
まずOpenCVで元動画を読み込み、width、height、fpsを取得します。これらは編集後のフレームを書き込むcv2.VideoWriter()で使用します。first_frameは元動画の最初のフレームです。
なお、コードを簡潔にするため、リアルタイムでカメラから入力する方式ではなく、動画ファイルを読み込む方式にしています。例外処理もありません。
seg_num = 180
seg_height = int(height / seg_num)
inputs = deque([first_frame], maxlen=seg_num)
output = first_frame.copy()
seg_numはフレームを分割するセグメントの数、seg_heightはその高さです。inputsは元動画から読み取ったフレームを格納しておくデックです。現在のフレームを合わせてseg_numフレームより前のフレームは参照しないため、サイズはseg_numに制限しています。outputはタイムディスプレイスメントを適用するフレームで、1つ目のフレームをベースにしています。以降はこのoutputに対して、遅延したセグメントで書き換えます。
for frame in frames(cap, seg_num - 1):
inputs.appendleft(frame)
for current_seg_num, frame in enumerate(inputs):
start = current_seg_num * seg_height
end = start + seg_height
seg = frame[start:end, :, :]
output[start:end, :, :] = seg
writer.write(output)
writer.release()
cap.release()
frames関数は後で載せていますが、capを受けて元動画のフレームを1つずつ返します。次のenumerate(inputs)のループでは、追加したフレームから順番に、フレームが1つ前へ戻るごとにセグメントの位置を下に移動しながらセグメントをoutputにコピーしています。これを図にすると以下のようになります。
ところで、frames関数は以下のようになっていて、2つ目の引数としてpaddingを受け取っています。
def frames(cap, padding):
while True:
ret, frame = cap.read()
if not ret:
break
yield frame
last_frame = frame
for _ in range(padding):
yield last_frame
capで元動画の最後のフレームまでyeildするのはいいとして、そのあとpaddingの数だけ最終フレームを繰り返しています。これは、タイムディスプレイスメントを適用した結果、動画の出力が途中で止まってしまうことへの対策です。
上の図で、デックの先頭、$i$の位置のフレームが元動画の最終フレームだとします。このとき、outputの$N$番目、一番下のセグメントは$N-1$フレーム前のセグメントを表示しています。しかし、元動画の入力はここまでのため、この位置のセグメントが$i$番目のフレームまで到達することはありません。1番目のセグメントを除くその他のセグメントもです。そのため、padding = seg_num - 1フレームを追加し、$N$番目のセグメントが最後まで表示されるようにしています。
では動画を用意しましょう
code.1〜4はGoogle Colabなどに貼り付ければそのまま動作します。お好きな動画でうねうねをお楽しみください。FPSは60が滑らかです。なお、背景も動いてしまうと画面全体が波打ってどうしようもなくなるので、カメラは必ず固定してください。

