概要
FFmpeg を使って、複数の動画を並べてフレーム数を表示する目的で使用する際の備忘録。
コマンド生成と FFmpeg 実行に python を利用する。(bat 化しても便利そう)
基本コマンド概要
最終的には以下のようなコマンドが実行される想定。肝心の複数の動画のレイアウトやフレーム数表示は後述。
ffmpeg.exe
# 上書き確認をしないフラグ
-y
# 入力 -r:入力フレームレート -stream_loop: ループ回数(0で1回)
-r 60 -stream_loop 1 -i input_01.mp4
-r 60 -stream_loop 0 -i input_02.mp4
# 出力動画のコーデック
-c:v h264
# 出力動画のフォーマット
-pix_fmt yuv420p
# 出力フレームレート
-frame_rate=60
# 無限ループ(gifで書き出す場合)
-loop 0
# filter
-filter_complex "後述"
# 出力ファイル
output.mp4
連番画像を入力する
入力に連番画像を並べる場合、例えば4桁のシーケンス番号を持つ input_0001.png
のような連番画像は以下のように記載する。
-i input_%04d.png
filter_complex
複数の処理(フィルタ)を複雑に組み合わせる場合、 filter_complex
コマンドを使う。
シンプルなフィルタを扱う場合も同じ知識が使えるため、ここでは filter_complex を前提として記載する。
フィルタごとは ,
フィルタの各パラメータは :
で区切る。
ざっくり以下のような全体像になる。
# fps
fps=60,
# 出力サイズ
scale=640:480,
# レイアウト
[0:v][1:v]xstack=inputs=...,
# テキスト描画
drawtext=text=hoge:fontfile=Sans:...,
drawtext=text=fuga:fontfile=Sans:...
複数動画を並べる
xstack
フィルタを利用して複数の入力を並べる。
xstack=
# 入力の数
inputs=2:
# 一番短い入力の終了時に強制終了するフラグ(0で無効)
shortest=0:
# レイアウト時の座標指示。'|'区切りで入力の分だけ記載する
layout=0_0|w0_0
レイアウト自動化
入力の数から自動でレイアウト指示を生成する。
layouts = []
# input の数の平方根から列の数を取得
sqrt = math.sqrt(input_length)
v_max = math.ceil(sqrt)
for i in range(input_length):
# レイアウトを生成
h, remainder = divmod(i, v_size)
v = remainder if not remainder == v_max else 0
w = '+'.join([f"w{x}" for x in range(0, v)]) if not v == 0 else "0"
h = '+'.join([f"h{x}" for x in range(0, h)]) if not h == 0 else "0"
layouts.append(f"{w}_{h}")
layout = '|'.join(layouts)
フレーム数を描画する
drawtext
フィルタを利用する。
drawtext=
# テキストの表示位置(左上基準)
x=0:y=0:
# 使用するフォントのファイルパス、またはフォントファミリー
fontfile=Sans:
# 文字色。文字列指定または [0x|#]RRGGBB[AA] のフォーマットで記載
fontcolor=0xff0000ff:
# 文字の大きさ
fontsize=24:
# テキストに box (座布団) を敷くフラグ
box=1:
# box のカラー
boxcolor=0x00000080:
# box のボーダー(box の外周と文字間のパディング)
boxborderw=10:
# テキスト。 frame_num(n) で現在のフレーム数
text=frame_num
テキストの描画位置自動化
box ボーダーの幅を加味した左右上下揃えの座標を算出する
# 横
if is_left:
# 左揃えの場合
x = f"{box_border}"
else:
# 右揃えの場合
x = f"w-text_w-{box_border}"
# 縦
if is_top:
# 上揃えの場合
y = f"{box_border}"
else:
# 下揃えの場合
y = f"h-text_h-{box_border}"
フレーム数を補正する
expr_int_format (eif)
を利用して、フレーム数に以下の補正を加える。
- カウントをサイクルする
- 動画をループ再生する際に1サイクル毎のフレーム数を表示する目的
- 表示を開始するフレームを指定
- 後述の「指定フレームのみ表示する」と併用を想定
- カウントの初期値を指定
digits = 3
if is_cycle:
frame = f"mod(n*{source_frame_rate / output_frame_rate},{one_cycle_frame_num})"
digits = max(digit, len(str(one_cycle_frame_num)))
else:
frame = "n"
text = f"%{{expr_int_format\:{frame}+{start_count - start_frame}\:d\:{digits}}}"
# sample
text=%{{expr_int_format\:n+0\:3}}
text=%{{expr_int_format\:mod(n*2,30)+10\:d\:2}}
指定のフレームのみ表示する
enable
フィルタ + between()
で範囲を指定する。
※ between() は文字列描画専用ではないので他の用途にも使える。
# sample
drawtext=
...:
# 範囲指定 (value, start, end)
enable=between(n,10,60)
Gif 動画を書き出す際の画質向上手段
こちらの記事に非常に丁寧にまとめられています。すばら
もろもろの処理の後にこちらのフィルターを追加すると画質向上や省サイズ化ができます。
-filter_complex "xstack=...,split [a][b];[a] palettegen [p];[b][p] paletteuse=dither=none"
Python から実行し、エラーを受け取る
python から FFmpeg を subprocess で叩き、実行結果を受け取りエラーハンドリングする。
※ ここはもっと良い手法がありそう。
result = subprocess.run(ffmpeg_command, shell=False, capture_output=True)
# ffmpeg のエラー文に特定の文字列が入っていたらエラーとみなす
if "Conversion failed!" in result.stderr.decode('utf-8'):
# エラー処理
is_error()