14
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

初めてのアドベントカレンダーAdvent Calendar 2021

Day 5

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

Last updated at Posted at 2021-11-01

はじめまして!Nancyと申します🌷

こちらはTeamうどんさんが主催されている,初めてのアドベントカレンダー 5日目の記事です🎄
わたしも初めてアドベントカレンダーに登録したので,ルール違反?などがあればご指摘をお願いいたします🙇‍♂️

はじめに

とあるハッカソンにて,動画生成を行うプロダクトを開発したのですが,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ファイルを生成します.

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

ソースコード

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を組み合わせることで,画像をクリップ化し繋ぎ合わせた動画を生成することができる

追記

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

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

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?