🌼

【OpenCV + MoviePy】画像から動画を自動生成してみた!

6 min read

はじめに

とあるハッカソンにて,動画生成を行うプロダクトを開発したのですが,MoviePyに関わる日本語の記事がほとんどなくかなり苦戦したため,自分でまとめてみることにしました!💐
1週間で調べたことなので,誤りなどがあれば気軽にコメントをしていただけると嬉しいです🔰

使用環境

  • Python 3.8.3
  • OpenCV 4.5.1
  • MoviePy 1.0.3
  • NumPy 1.18.5

今回使用する画像

みなさんご存知のLennaさんをお借りします.

Lenna.png

今回生成する動画

今回は0.5秒ごとにLennaさんが拡大と縮小を繰り返すgifファイルを生成します.

Zennにmp4ファイルをアップロードすることができなかったため今回はgifファイルですが,mp4ファイルも書き出すことができます.


Lenna.gif

ソースコード

import cv2
import moviepy.editor as mpy
import numpy as np

# 変数
MOVIE_LENGTH = 10
FPS = 30
MOVIE_FRAMES = MOVIE_LENGTH * FPS
SECONDS_PER_FRAME = 1 / FPS
BASE_SIZE = 250
BASE_COLOR = [255, 255, 255]
clips = []

# 画像の読み込み
lenna_origin = cv2.imread('Lenna.png')
lenna_origin = cv2.cvtColor(lenna_origin, cv2.COLOR_BGRA2RGBA)

# 背景画像の準備
base_img = np.full((BASE_SIZE, BASE_SIZE, 3), BASE_COLOR)
base_clip = mpy.ImageClip(base_img).set_duration(MOVIE_LENGTH)

# 画像を円形に切り出し,クリップに変換する
for i in range(MOVIE_FRAMES):
    # 深いコピー
    lenna = lenna_origin.copy()

    # 画像の拡大縮小
    if i % FPS < FPS // 2:
        new_size = 200 - 50 * (i % 15) // 15
    else:
        new_size = 150 + 50 * (i % 15) // 15
    lenna = cv2.resize(lenna, dsize=(new_size, new_size))

    # マスク処理
    mask = np.zeros((new_size, new_size))
    cv2.circle(mask, center=(new_size//2, new_size//2), radius=new_size//2, color=255, thickness=-1)
    lenna[mask==0] = [0, 0, 0, 0]

    # 画像をクリップ化
    clip = mpy.ImageClip(lenna).set_duration(SECONDS_PER_FRAME)
    clips.append(clip)

# 動画を作成する処理
lenna_clip = mpy.concatenate_videoclips(clips)
clip.close()

# クリップの合成
final_clip = mpy.CompositeVideoClip([base_clip, lenna_clip.set_position(('center'))])
final_clip.write_gif(filename = 'Lenna.gif', fps=FPS)
final_clip.close()

急に長々としたソースコードで訳が分からないと思うので,細かく見ていきたいと思います.

変数

MOVIE_LENGTH = 10
FPS = 30
MOVIE_FRAMES = MOVIE_LENGTH * FPS
SECONDS_PER_FRAME = 1 / FPS
BASE_SIZE = 250
BASE_COLOR = [255, 255, 255]
clips = []

今回はこのような変数を用います.

  • MOVIE_LENGTH : 作成する動画の長さ(秒)
  • FPS : 作成する動画のFPS(30または60)
  • MOVIE_FRAMES : 作成する動画のフレーム数
  • SECONDS_PER_FRAME : フレーム1枚の長さ(秒)
  • BASE_SIZE : 背景画像のサイズ(pixel)
  • BASE_COLOR : 背景画像のRGB値(今回は白)
  • clips : 作成したクリップを格納するための空配列

画像の読み込み

lenna_origin = cv2.imread('Lenna.png')
lenna_origin = cv2.cvtColor(lenna_origin, cv2.COLOR_BGRA2RGBA)

Lennaさんの画像を読み込みます.後から透過処理を行うため,BGR画像からRGBA画像へ変更します.

背景画像の準備

base_img = np.full((BASE_SIZE, BASE_SIZE, 3), BASE_COLOR)
base_clip = mpy.ImageClip(base_img).set_duration(MOVIE_LENGTH)

全ての要素がBASE_COLORで,大きさが(BASE_SIZE, BASE_SIZE, 3)Numpy型配列base_imgを準備します.そして,base_imgを長さMOVIE_LENGTHのクリップ(base_clip)に変換します.

画像を円形に切り出し,クリップに変換する

for i in range(MOVIE_FRAMES):
    # 深いコピー
    lenna = lenna_origin.copy()

    # 画像の拡大縮小
    if i % FPS < FPS // 2:
        new_size = 200 - 50 * (i % 15) // 15
    else:
        new_size = 150 + 50 * (i % 15) // 15
    lenna = cv2.resize(lenna, dsize=(new_size, new_size))

    # マスク処理
    mask = np.zeros((new_size, new_size))
    cv2.circle(mask, center=(new_size//2, new_size//2), radius=new_size//2, color=255, thickness=-1)
    lenna[mask==0] = [0, 0, 0, 0]

    # 画像をクリップ化
    clip = mpy.ImageClip(lenna).set_duration(SECONDS_PER_FRAME)
    clips.append(clip)

ここでは1フレームごとの画像を生成し,画像をクリップに変換します.

深いコピー

    lenna = lenna_origin.copy()

ここで,あらかじめcv2.imread()しておいたLennaさんを,深いコピーします.

注意
今回は画像の切り抜きかたが全てのフレームで同じため,浅いコピーでもいいのですが,フレームによって切り抜き方を変えたい(画像サイズは同じで,切り抜く大きさを変えたいなどの)場合には,浅いコピー(lenna = lenna_origin)をしてしまうと,挙動が変わってしまいます.

画像の拡大縮小

    if i % FPS < FPS // 2:
        new_size = 200 - 50 * (i % 15) // 15
    else:
        new_size = 150 + 50 * (i % 15) // 15

今回は30FPSのため,

  • 現在のフレーム数iを30(FPS)で割ったあまり(i % FPS)が15(FPS // 2)より小さい場合には,200(pixel)から150(pixel)まで縮小
  • 現在のフレーム数iを30(FPS)で割ったあまり(i % FPS)が15(FPS // 2)以上の場合には,150(pixel)から200(pixel)まで拡大

するようにnew_sizeを決定しました.

マスク処理

    mask = np.zeros((new_size, new_size))
    cv2.circle(mask, center=(new_size//2, new_size//2), radius=new_size//2, color=255, thickness=-1)
    lenna[mask==0] = [0, 0, 0, 0]

全ての要素が0(黒)で,大きさが画像の拡大縮小で決定した(new_size, new_size)Numpy型配列maskを準備します.
円を描画する関数cv2.circle()を利用して,マスクの残したい部分である,

  • center = (new_size//2, new_size)//2 : 中心の座標
  • radius = new_size//2 : 半径
  • color = 255 : 色(白)
  • thickness = -1 : 線の太さ(塗りつぶし)

を描画します.
lennaのうち,maskの値が0の画素は透過(アルファチャネルを0に)することで,マスク処理ができました.

画像のクリップ化

    clip = mpy.ImageClip(lenna).set_duration(SECONDS_PER_FRAME)
    clips.append(clip)

画像の読み込みと同様に,lennaを長さSECONDS_PER_FRAMEのクリップ(clip)に変換し,clipsに格納します.

クリップをつなぎ合わせる

lenna_clip = mpy.concatenate_videoclips(clips)
clip.close()

画像のクリップ化で生成したclipsのクリップをmoviepy.editor.concatenate_videoclips()を用いてつなぎ合わせます.
メモリリークを防ぐために,使い終わったらclose()をしましょう.

クリップの合成

final_clip = mpy.CompositeVideoClip([base_clip, lenna_clip.set_position(('center'))])
final_clip.write_gif(filename = 'Lenna.gif', fps=FPS)
final_clip.close()

最後に,作成したクリップをmoviepy.editor.CompositeVideoClip()を用いて合成します.set_position()を用いることで,1つ目のクリップから見た相対座標に重ねることができます.
write_gif()を用いてgifファイルを書き出します.

write_gif()の代わりにwrite_videofile() を用いれば,mp4ファイルで書き出せます.

ここでも忘れずに,close()をしてください.

まとめ

  • OpenCVとMoviePyを組み合わせることで,画像をクリップ化し繋ぎ合わせた動画を生成することができる

追記

この技術を用いたプロダクトが「バンダイナムコ研究所賞」を受賞することができました!🧡
初めてハッカソンで受賞できたので,とても嬉しいです💐

その他の企業賞・特別賞作品はこちらからご確認いただけます!

Discussion

ログインするとコメントできます