ffmpeg でスライドショーを作る

ffmpeg で連続画像からクロスフェードスライドショーを(無理やり)作成したので、 概要をメモしておきます。

å°Žå…¥

動機としては、画像生成 AI で作った画像を比較するのに動画でできないかなと思った次第です。

ビュアーで一枚一枚見ることもできるのですが、 二次元的に比較したい (例えば「シチュエーション」x「パラメータ」みたいな) 場合、 二つビュアーを立ち上げてそれぞれ別々に動かすのは面倒です。 面倒だけならまだしも、その手間が邪魔をして印象が失われてしまいます。 また、本当に細かい違いをみる場合は瞬間で切り替わるのがいいのですが、 雰囲気的なものを確認したい場合はむしろクロスフェードの方が向いています (個人の感想です)。

XYZ グリッド生成もありますが、 微妙な差を確認したい場合には別々に並んでいる画像を見て判断するのは難しいものがあります。 それに、本当に大量生成したい場合にはグリッドにすることすらできません。

という訳で、大量の画像から、グリッドで動画でクロスフェードスライドショーしてくれるようにする、 ffmpeg のコマンドを作りました。

正確には、「『それをする ffmpeg のコマンド』を生成するスクリプト」を作りました。 というのも、最初はもっと簡単に済むと思っていたのですが、 かなり長いスクリプトを書かないといけないと分かって、 ごり押ししたためです。

そんな事情もあるので、出来はお察しください。 ちゃんと python なりで書いた方が読みやすいものができると思います。 そもそも ffmpeg の仕様もちゃんとわかっていませんし。

コード

四の五の言う前にコードを置いておきます。 powershell です。

備忘録なので動作は保証しません。 自己責任でご利用ください。念のため。

1 タイルのスライドショー

コードを表示

$secPerPict = 1.0 
$file_ext = "webp"

$inputs = ""
$filter = ""
$overlay = "[0][f0]overlay[bg1];"
$prev = 0
$index = 0
$count = 0
$fade = $secPerPict / 5.0
$offsetPerPict = $secPerPict - ($fade / 2.0)

$files = Get-ChildItem -Path ".\" -Filter "*.$file_ext"
foreach ($f in $files) {
    if ($count -ge 3) {
        $overlay += "[bg$prev][f$prev]overlay[bg$index]; `n"
    }
    $prev = $index
    $index = $count
    $count += 1
    $name = $f.Name
    $tmp1 = $name -Split "-"
    $tmp2 = $tmp1[1] -Split "@"
    $text = $tmp2[0]
    $inputs += "-loop 1 -t $secPerPict -i '$name' `n"
    if ($count -ge 2) {
        $offset = $offsetPerPict * $count
        $text_part = "drawtext=text='$text':fontsize=80:x=20:y=20:box=1:boxcolor=white"
        $filter += "[$index]$text_part,fade=d=0.1:t=in:alpha=1,setpts=PTS-STARTPTS+$offset/TB[f$prev]; `n"
    }
}
$overlay += "[bg$prev][f$prev]overlay,format=yuv420p[v]"
$cmd = "ffmpeg -y $inputs -filter_complex `n `"$filter$overlay`" "
$cmd += '-map "[v]" '
$cmd += '-r 60 '
$cmd += 'output.mp4'

$exec_cmd = "$cmd" -replace "`n",""
Set-Clipboard -Value $exec_cmd
Invoke-Expression  "$exec_cmd"

仕様は以下のような感じです。 変更したい場合はコードを弄ってください。

  • 入力
    • カレントディレクトリの複数の画像 (4枚以上)。
    • ファイル名は xxx-<text>@xxx.png。
      • xxx は - ã‚„ @ を含まない任意の文字列。
      • <text> は画像に表示する文字列。
      • 拡張子は下記の変数で設定可能。
  • 設定値
    • secPerPict: 画像一枚の表示時間
    • file_ext: ファイル拡張子 (png とか webp とか)
  • 出力
    • output.mp4 という名前の動画。
    • 画像表示時間の 1/5 の時間でクロスフェード。
    • 左上に白背景の黒テキストを表示。
    • 60 fps。

連結用のコマンド

先ほどのスクリプトを複数のディレクトリで実行して、 生成した動画を縦に並べます。

ffmpeg `
    -i dir_hogehoge\output.mp4 `
    -i dir_fugafuga\output.mp4 `
    -filter_complex "vstack" output.mp4

横に並べるとかグリッドにするとかもできると思います。*1

コマンドの解説

メモ程度に、なんでこんなスクリプトを書くことになったのかという原因、 つまり、 ffmpeg の仕様とその限界をまとめておきます。

当初の想定

当初は、以下のようなコマンドで何とかしたいなと思っていました。 (引数はまったくの適当です)

ffmpeg -input *.webp -option_hogehoge "fade_fugafuga" ... output.mp4

単純に連結するだけならすぐに見つかって、 例えば以下のようなコマンドで良いようです。 *2

$ ffmpeg -framerate 30 -i image_%03d.webp -vcodec libx264 -pix_fmt yuv420p -r 60 output.mp4

これでカレントディレクトリにある連番のファイルを切り替えながら表示することができます。 でも、それでいいならわざわざ動画を作らなくてもいいので。

他の方法の模索

なのでいろいろ探してみたのですが、簡単な方法は見つかりません。 動画自体にクロスフェードをかけたりはあるようでしたが、 画像の連結にそれをする方法は見つかりませんでした。

やっと見つかったのは、画像ごとに処理を書く方法です。 以下のコマンドは、 5枚の画像を連結して、 3秒間表示しつつ1秒かけてクロスフェードする処理です。 *3

ffmpeg \
-loop 1 -t 3 -i img001.jpg \
-loop 1 -t 3 -i img002.jpg \
-loop 1 -t 3 -i img003.jpg \
-loop 1 -t 3 -i img004.jpg \
-loop 1 -t 3 -i img005.jpg \
-filter_complex \
"[1]fade=d=1:t=in:alpha=1,setpts=PTS-STARTPTS+2/TB[f0]; \
 [2]fade=d=1:t=in:alpha=1,setpts=PTS-STARTPTS+4/TB[f1]; \
 [3]fade=d=1:t=in:alpha=1,setpts=PTS-STARTPTS+6/TB[f2]; \
 [4]fade=d=1:t=in:alpha=1,setpts=PTS-STARTPTS+8/TB[f3]; \
 [0][f0]overlay[bg1];[bg1][f1]overlay[bg2];[bg2][f2]overlay[bg3]; \
 [bg3][f3]overlay,format=yuv420p[v]" -map "[v]" -r 25 output.mp4

細かく見ていくと。

  • loop の部分は、入力ファイルと表示時間 (-t)。
    • 入力ファイルの数だけ指定する。
  • [n]fade... の部分はフェード処理を定義。
    • fade=d=X の X でフェード持続時間を指定。
    • PTS-STARTPTS+X の X でフェード開始時間を指定。
    • 入力ファイルの数より一だけ小さい回数指定する。
  • [bgN][fN]overlay[bgN+1];の部分は、それらを接続している (っぽい)。

実際、この方法を使えば思い通りの動画ができました。

しかし、この方法の問題は、入力ファイルの数だけ大量にオプションやらなんやらを指定する必要があることです。 他にもっといい方法がありそうですが、調べなおすのも面倒だったのでごり押ししました。

それが先に挙げたコードです。 ファイルの一覧を取得して、その数だけ上記のオプションを追加して、 最後に Invoke-Expression で実行する、というごり押しです。

字幕

この方法を使ったことによる利点として、 各スライドに字幕をつけることが可能となりました。

具体的には、[n]fade... の部分に処理を追加します。 以下に、スクリプト中のものを抜粋します。

drawtext=text='$text':fontsize=80:x=20:y=20:box=1:boxcolor=white

これで $text の中身が、左上から 20x20 の部分に、フォントサイズ 80 で、 白背景で出力されます。 *4

ただ、最初の画像にだけは字幕が表示されないバグがあります。 直すのが面倒なのと、実用上問題ないのでそのままにしています。

生成例

実際に生成してみたものは Twitter に。

*1:「知りませんけど。」

*2:参考: https://qiita.com/livlea/items/a94df4667c0eb37d859f

*3:参考: https://www.bannerbear.com/blog/how-to-create-a-slideshow-from-images-with-ffmpeg/#slideshow-with-a-crossfade-transition

*4:デフォルトのフォントが何かは分かりませんが、 英語以外の文字列を表示する場合はちゃんと指定しないとダメかもしれません (試していません)。