はじめまして!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を組み合わせることで,画像をクリップ化し繋ぎ合わせた動画を生成することができる
追記
この技術を用いたプロダクトが「バンダイナムコ研究所賞」を受賞することができました!🧡
初めてハッカソンで受賞できたので,とても嬉しいです💐
その他の企業賞・特別賞作品はこちらからご確認いただけます!