えらく間隔が空いてしまいましたが、「指揮付きの伴奏動画を作る:スコアとガイドの表示」の続きです。こういう動画を作成します(画面サイズを1280x720から960x720に変更しました)。
pygame で画面を描画して、OpenCV (cv2) で mp4 として書き出す、という方針で進めます。まず、必要なモジュールのインポートと、定数定義です。
import pygame
import numpy as np
import cv2
WIDTH = 960 # Screen width
HEIGHT = 720 # Screen height
FPS = 30.0 # Frames per second
pygame の画面と OpenCV の書き出しオブジェクトを初期化します。
# Initinalize pygame screen and video output stream
# screen size is (WIDTH, HEIGHT)
def init_screen():
global screen, videosave, mask
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Make Movie Test")
history = []
fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
videosave = cv2.VideoWriter('movie_nosound.mp4', fourcc, FPS, (WIDTH, HEIGHT))
mask = np.zeros((HEIGHT, WIDTH, 3), dtype="uint8") # mask for fade-in/out
pygame の画面を更新する関数です。beatlist を読んで、指揮棒の位置を計算するところまでは、前と同じです。
def update_screen(frame, beatlist, index, fade = 1.0):
global page_images, staff_rects, screen, videosave, mask, title_image
etime = frame * 1000000.0 / FPS # Elapsed time in microseconds
i = index + 1
while i < len(beatlist) and beatlist[i][0] <= etime:
i += 1
i -= 1
# etime is in the time interval (t(i), t(i+1)) (where "t(i)" means beatlist[i][0])
rect = staff_rects[beatlist[i][1]] # Staff rect of the current page
if etime < 0 or i == len(beatlist) - 1:
# The play indicator is fixed at the bar position
x = int(beatlist[i][2])
else:
dt = etime - beatlist[i][0] # delta time from the beat position
dt_next = beatlist[i + 1][0] - beatlist[i][0] # time interval between the two beats
x_base = beatlist[i][2] # X position of the last beat
x_next = beatlist[i + 1][2] # X position of the next beat
if x_next < x_base: # next beat is in the new page?
x_next = rect[2] # staff width (= right edge of the staff)
x = x_base + int((x_next - x_base) * dt / dt_next) # X position of the present moment
pygame の画面をクリアして、タイトル・楽譜・マーカーの縦線をそれぞれ描画します。
screen.fill((0, 0, 50)) # clear screen
# show title
screen.blit(title_image, (0, 0))
# show page and marker bar
image = page_images[beatlist[i][1]]
ybase = HEIGHT - image.get_height()
screen.blit(image, (0, ybase))
x += rect[0]
y0 = ybase + rect[1] - int(unit2pixel * 2)
y1 = ybase + rect[1] + rect[3] + int(unit2pixel * 2)
pygame.draw.line(screen, (255, 0, 0), (x, y0), (x, y1), 3)
指揮図を書きます。このあたりのコードは、「指揮付きの伴奏動画を作る:式の表示」で書いたものとほぼ同じです。
# conductor
history = []
j = i
for k in range(15):
# calculate conductor position for previous 15 moments (5 moments per frame)
et = etime - k * 1000000.0 / (FPS * 3)
if et <= 0:
beat = beatlist[0]
pos_x, pos_y = conductor_pos(beat[4], beat[5], beat[6], beat[7], 0)
elif etime >= beatlist[-1][0]:
pos_x = 0
pos_y = 1
else:
while et < beatlist[j][0]:
j -= 1
b = (et - beatlist[j][0]) / (beatlist[j + 1][0] - beatlist[j][0])
if b < 0.5:
beat = beatlist[j]
pos_x, pos_y = conductor_pos(beat[4], beat[5], beat[6], beat[7], b)
else:
beat = beatlist[j + 1]
pos_x, pos_y = conductor_pos(beat[4], beat[5], beat[6], beat[7], b - 1.0)
pos_x = int((pos_x + 1) * 0.5 * WIDTH)
pos_y = int(pos_y * (HEIGHT - max_height - 40)) + 20
history.append((pos_x, pos_y))
for ih in range(len(history) - 1, -1, -1):
pygame.draw.circle(screen, (0, 255 - ih * 17, 0), history[ih], 20, 0)
# Update only occasionally (no need to show every frame; actually, updating is just for showing where we are now)
if frame % 50 == 0:
pygame.display.update()
描画したスクリーンを読み出して、動画の1フレームとして書き出します。この時、動画終了時のフェードアウトも同時に処理します。あとでエフェクトをかけてもよいのですが、エンコードし直すと時間がかかるので、簡単なエフェクトなら作成時にやっておいた方がよいのです。
# Video output
s = pygame.image.tostring(screen, 'RGB', False)
s = np.frombuffer(s, np.uint8).reshape(HEIGHT, WIDTH, 3)
if fade < 1.0:
s = cv2.addWeighted(s, fade, mask, 1 - fade, 0)
s = cv2.cvtColor(s, cv2.COLOR_RGBA2BGR)
videosave.write(s)
return i
先頭と最後に2秒のブランクを入れて、全フレームを書き出します。
# blank 2 seconds
for n in range(int(FPS) * 2):
update_screen(-1, beatlist, index)
# Process frames
frame = 0
while index < len(beatlist) - 1:
index = update_screen(frame, beatlist, index)
frame += 1
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
# blank 2 seconds
for n in range(int(FPS) * 2):
fade = min(1.0, (int(FPS) * 2 - n) / FPS / 0.5) # Fade out
update_screen(frame, beatlist, index, fade)
#pygame.time.wait(int(1000 / FPS))
videosave.release()
ffmpeg を使って音声ファイルと結合します。
# Compose with the sound file
os.system("ffmpeg -y -i movie_nosound.mp4 -itsoffset 2.1 -i {} -async 1 -vcodec copy -strict -2 movie.mp4".format(soundfile))
Python を使って動画ファイルを作成するのは応用範囲が広そうです。使えるテクニックが一つ増えました。