Pythonで電車揺れのアニメーションを作ろう
やりたいこと
まず揺らす対象の画像を用意します
これは僕が適当に描いた電車の車内です

走行中の音源はこちらを使います
この音源に合わせて先ほどの画像を良い感じに揺らそうというのが今回の目的です
↓こんな感じになります
やること
揺らすために以下の二つの処理をします
- 音源を読み込んでデータを最大値で正規化する
- 乱数で揺らす方向のベクトルを生成して揺らす
1.音源を読み込んでデータを最大値で正規化する
librosaを使います
import librosa
class AudioInformation:
def __init__(self, audio_file):
self.data, self.sr = AudioInformation.parse_audio(audio_file)
self.length = librosa.get_duration(y=self.data, sr=self.sr)
self.pre_idx = 0
def get_data_at(self, sec):
idx = int(self.sr * sec)
res = max(self.data[self.pre_idx:idx+1])
self.pre_idx = idx + 2
return res
@staticmethod
def parse_audio(audio_file):
y, sr = librosa.load(audio_file)
return abs(y) / max(abs(y)), sr
get_data_atは
ピンポイントのデータを取ると微妙に音源と合わないということになるのでやらない方がいいです
2.乱数で揺らす方向のベクトルを生成して揺らす
「向き」と「大きさ」を指定すればベクトルになります
向きをx軸正方向とのなす角
ですので、
まず
単純に揺れの大きさは音の強さに比例すると考えて、先ほどの音源データがちょうと
揺れの最大値をget_data_atの返り値とすると
です
いくらランダムな方向に揺らすとは言え、横方向への揺らしは何だか変な感じがします
加えて、直前の揺れと同じ方向へ揺らすのも変です
以上を踏まえて、
以上で乱数生成部は終わりです
次は実際にそのベクトルに沿って画像を移動させるのですが、これにアフィン変換を使います
アフィン変換は、簡単に言うと以下の式で表される操作です
この式一つで移動、回転、変形などの操作が出来るというんですから、凄いですよね
ちなみに三行目は使いません
じゃあなんであんだよと思うかもしれませんが、
まあ今回は移動しかやらないので、以下の形を使用します
コードを書く
以上を踏まえてコードを書きます
import cv2
import numpy as np
import librosa
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.audio.io.AudioFileClip import AudioFileClip
from PIL import Image
import math
import os
import random
import sys
SLIGHT_VIB_SIZE = 9
MAX_LARGE_VIB_SIZE = 1
LARGE_VIB_BORDER = 0.3
FPS = 30
INTERVAL = 1000 / FPS
class AudioInformation:
def __init__(self, audio_file):
self.data, self.sr = AudioInformation.parse_audio(audio_file)
self.length = librosa.get_duration(y=self.data, sr=self.sr)
self.pre_idx = 0
def get_data_at(self, sec):
idx = int(self.sr * sec)
res = max(self.data[self.pre_idx:idx+1])
self.pre_idx = idx + 2
return res
@staticmethod
def parse_audio(audio_file):
y, sr = librosa.load(audio_file)
return abs(y) / max(abs(y)), sr
def cvt2Image(data):
return Image.fromarray(cv2.cvtColor(data, cv2.COLOR_BGR2RGB))
def shift(data, dx, dy):
mat = np.float32([[1, 0, dx], [0, 1, dy]])
return cvt2Image(cv2.warpAffine(data, mat, data.shape[:2][::-1]))
def anim_shift(data, dx, dy):
res = []
for i in range(5, 11, 5):
r = i / 10
res.append(shift(data, dx * i, dy * i))
res += res[:-1][::-1]
return res
def _shake(raw_image, frames, audio_info):
while True:
try:
data = audio_info.get_data_at(len(frames) / FPS)
except ValueError:
break
if len(frames) % 2 == 0:
t = random.uniform(math.pi / 4, math.pi * 3 / 4)
else:
t = random.uniform(math.pi * 5 / 4, math.pi * 7 / 4)
if LARGE_VIB_BORDER < data:
size = MAX_LARGE_VIB_SIZE * data
res = anim_shift(raw_image, size * math.cos(t), size * math.sin(t))
frames.extend(res)
else:
size = SLIGHT_VIB_SIZE * max([data, 0.1])
res = shift(raw_image, size * math.cos(t), size * math.sin(t))
frames.append(res)
for _ in range(2):
frames.append(cvt2Image(raw_image.copy()))
def shake(image_file, audio_info):
raw_image = cv2.imread(image_file)
frames = []
_shake(raw_image, frames, audio_info)
dst = os.path.splitext(image_file)[0] + ".gif"
src = cvt2Image(raw_image)
src.save(dst, save_all=True, append_images=frames, duration=INTERVAL, loop=0)
return dst
def main():
"""
第一引数: 揺らす画像
第二引数: 走行音
"""
a = AudioInformation(sys.argv[2])
gif = shake(sys.argv[1], a)
clip = VideoFileClip(gif)
audio = AudioFileClip(sys.argv[2]).subclipped(0, clip.duration)
clip.with_audio(audio).write_videofile(os.path.splitext(sys.argv[1])[0] + ".mp4")
if __name__ == "__main__":
main()
小揺れと大揺れで揺れの最大値を変えてます
この辺りは実際に結果を見ながら調整しました
また、音源データがどの大きさから「大揺れ」とするかのボーダーも色々見ながら良い感じのにしました
あんまり揺れが大きすぎると画面酔いするのでほどほどの値にしておきましょう
あと大揺れの時は変化が大きいので、アニメーション的な変化をさせてます(ここ、フレーム数の偶奇が変わるので、直後が同じ方向に動いてしまうのですが、見なかったことにします)
結論(?)
Pythonって、便利ですね
Discussion