前提
コマンドラインで FFmpeg を使い、映像にフィルターをかけていく。
環境
- FFmpeg 6.0-essentials_build-www.gyan.dev(Chocolatey 1.3.1 により導入)
- GNU bash 5.2.15(1)-release(Git for Windows 2.24.0 により導入)
- Windows 10 Home 64-bit 22H2
基本文法
映像にフィルターをかけるにはこうする。
ffmpeg -i "入力ファイル.avi" -filter:v "フィルター文字列" "出力ファイル.mp4"
# Qiita だと長いワンライナーは横スクロール必至で見にくいので、改行を挟んで書くことにする
ffmpeg -i "入力ファイル1.avi" -i "入力ファイル2.avi" \
-filter_complex "フィルター文字列" \
"出力ファイル.mp4"
単色映像ソースを呼び出す
入力指定オプション -i
は普通、-i "ファイル.avi"
のようにファイル名を指定する。
しかし、フォーマット強制オプション -f
と特殊な仮想入力デバイス lavfi
を用いて -f lavfi -i "映像ソース系フィルター"
のように指定すると、何のファイルというわけでもない新規映像ソースを描画して入力とすることができる。
映像ソース系フィルターには例えば color
があり、-f lavfi -i "color=color=pink"
とするとピンク色(#FFC0CB
)一色で塗り潰された映像ソースが生成される。
サイズ等の更なるパラメーターもあったりする。
ffmpeg `# デフォルトだとログ出力がやかましいので最低限のエラー報告に抑える` \
-loglevel warning \
`# 100×100 ピクセルのピンク色を描画` \
-f lavfi -i "color = color=pink: size=100x100" \
`# 静止画 1 枚として出力、既存ファイルを上書き` \
-update 1 -frames:v 1 -y "pink.png"
また、映像ソース系フィルターは当然フィルターなので、その場(-i
オプションの中)で更に色々な追いフィルターもかけられる。
そしてもちろん、その後段でも -filter:v
や -filter_complex
によってもっと好き放題にフィルターを重ねられる。
困り事:overlay
で映像を重ねられるが、フリンジが出る
ではここで、2 個の映像ソースを新規描画してから、overlay
フィルターで単純に重ねてみると…
ffmpeg -loglevel warning \
`# 左側がピンク色、右側が透明の 100×100 ピクセルの背景を描画` \
-f lavfi -i "color = color=pink: size=50x100,
format = rgba,
pad = w=in_w*2: color=black@0" \
`# やや傾いた 65×65 ピクセルの水色の正方形を描画` \
-f lavfi -i "color = color=cyan: size=65x65,
format = rgba,
pad = width=100: height=out_w: x=-1: y=-1: color=black@0,
rotate = angle=30*PI/180: fillcolor=none" \
`# 水色側を前景にしてシンプルに重ねる` \
-filter_complex "[0] [1] overlay = format=rgb" \
-update 1 -frames:v 1 -y "out0-just-overlay.png"
…ちょっと問題点がわかりづらいので拡大する。
ffmpeg -loglevel warning \
`# さっき出力した画像を入力` \
-i "out0-just-overlay.png" \
`# 上部だけを切り出してニアレストネイバー法で拡大` \
-filter:v "crop = y=0: h=40,
scale = width=in_w*6: height=-1: flags=neighbor" \
-update 1 -frames:v 1 -y "out0-just-overlay-zoom.png"
なんか…前景のフチが黒いんだよね……。
こいつを解決したい。
解決策
pad
の色埋めを工夫する…のは汎用性が無い
既に使っているが、色@不透明度
という記法は色に不透明度を追加できる。
black@0
なら rgba(0, 0, 0, 0)
、blue@0.4
なら rgba(0, 0, 255, 0.4)
という感じになる。
今回の例では前景が水色一色とわかりきっているので、pad
フィルターが前景に与える透明部分の色を black@0
ではなく cyan@0
にしてやれば、一応は解決する。
ffmpeg -loglevel warning -y `# -y は全ての出力に効くので手前に移動` \
-f lavfi -i "color = color=pink: size=50x100,
format = rgba,
pad = w=in_w*2: color=black@0" \
`# 余白に透明な水色を用いる` \
-f lavfi -i "color = color=cyan: size=65x65,
format = rgba,
pad = width=100: height=out_w: x=-1: y=-1: color=cyan@0,
rotate = angle=30*PI/180: fillcolor=none" \
`# 拡大した映像も同時に作っちゃう` \
-filter_complex "[0] [1] overlay = format=rgb,
split [normal] [tmp];
[tmp] crop = y=0: h=40,
scale = width=in_w*6: height=-1: flags=neighbor [zoom]" \
`# 普通のと拡大したのをそれぞれ別ファイルに出力` \
-map "[normal]" -update 1 -frames:v 1 "out1-alpha-cyan.png" \
-map "[zoom]" -update 1 -frames:v 1 "out1-alpha-cyan-zoom.png"
しかし当然ながら、この方法は前景の色を完全に掌握していないと使えない。
もっと汎用性の高い方法があるはず。
alpha=premultiplied
で解決…するのは不透明背景の上だけ
そもそもなんでこんなフリンジ(縁、輪郭線、境界線)が発生しているのかというと。いわゆるプリマルチプライ(premultiply)で事故っているため。
解決法として、overlay
フィルターに alpha=premultiplied
パラメーターを渡して「もうプリマルチプライしてあるよ!」と教えてあげる手法が知られているっぽい。
ffmpeg -loglevel warning -y \
-f lavfi -i "color = color=pink: size=50x100,
format = rgba,
pad = w=in_w*2: color=black@0" \
-f lavfi -i "color = color=cyan: size=65x65,
format = rgba,
pad = width=100: height=out_w: x=-1: y=-1: color=black@0,
rotate = angle=30*PI/180: fillcolor=none" \
`# alpha=premultiplied で重ねる` \
-filter_complex "[0] [1] overlay = format=rgb: alpha=premultiplied,
split [normal] [tmp];
[tmp] crop = y=0: h=40,
scale = width=in_w*6: height=-1: flags=neighbor [zoom]" \
-map "[normal]" -update 1 -frames:v 1 "out2-pre.png" \
-map "[zoom]" -update 1 -frames:v 1 "out2-pre-zoom.png"
うんうん、確かにピンク色の背景との間にあったフリンジが消えて…
透明背景の方にはフリンジ残ってるじゃん!!
unpremultiply
で(ほぼ)解決
unpremultiply
フィルターを inplace=1
パラメーターでかけてやれば、映像のアルファチャンネルがプリマルチプライドからストレートに変換される。
その際、逆に overlay
フィルターの alpha=premultiplied
パラメーターは指定しないでおくこと。
ffmpeg -loglevel warning -y \
-f lavfi -i "color = color=pink: size=50x100,
format = rgba,
pad = w=in_w*2: color=black@0" \
`# unpremultiply を噛ませる` \
-f lavfi -i "color = color=cyan: size=65x65,
format = rgba,
pad = width=100: height=out_w: x=-1: y=-1: color=black@0,
rotate = angle=30*PI/180: fillcolor=none,
unpremultiply = inplace=1" \
`# alpha=premultiplied は使わない` \
-filter_complex "[0] [1] overlay = format=rgb,
split [normal] [tmp];
[tmp] crop = y=0: h=40,
scale = width=in_w*6: height=-1: flags=neighbor [zoom]" \
-map "[normal]" -update 1 -frames:v 1 "out3-unpre.png" \
-map "[zoom]" -update 1 -frames:v 1 "out3-unpre-zoom.png"
背景の透明度によらず、前景のフリンジが消えた!
このシンプルな解決法が、調べても意外と全然見つからなくて苦労した。
シンプルすぎるからだろうか。
なお、もし alpha=premultiplied
パラメーターを消さずに残しておくと…
ffmpeg -loglevel warning -y \
-f lavfi -i "color = color=pink: size=50x100,
format = rgba,
pad = w=in_w*2: color=black@0" \
-f lavfi -i "color = color=cyan: size=65x65,
format = rgba,
pad = width=100: height=out_w: x=-1: y=-1: color=black@0,
rotate = angle=30*PI/180: fillcolor=none,
unpremultiply = inplace=1" \
`# alpha=premultiplied も指定してみる` \
-filter_complex "[0] [1] overlay = format=rgb: alpha=premultiplied,
split [normal] [tmp];
[tmp] crop = y=0: h=40,
scale = width=in_w*6: height=-1: flags=neighbor [zoom]" \
-map "[normal]" -update 1 -frames:v 1 "out4-unpre-pre.png" \
-map "[zoom]" -update 1 -frames:v 1 "out4-unpre-pre-zoom.png"
今度は逆に不透明背景の方に白いフリンジが発生してしまう。
何事もやり過ぎは良くない。
しかし実は完全解決ではない
unpremultiply
フィルターがおおよそ今回求めていたものであることはわかった。
でも、もうちょっと確認しておきたい。
背景の右側の透明化をやめて全てピンク色だけにし、更に前景の正方形をピンク色にして重ねてみる。
unpremultiply
フィルターのおかげでフリンジも出ないんだから、当然ピンク色一色だけの画像が出力されるはず。
ffmpeg -loglevel warning -y \
`# ピンク色の 100×100 ピクセルの背景を描画` \
-f lavfi -i "color = color=pink: size=100x100,
format = rgba" \
`# やや傾いた 65×65 ピクセルのピンク色の正方形を描画して unpremultiply` \
-f lavfi -i "color = color=pink: size=65x65,
format = rgba,
pad = width=100: height=out_w: x=-1: y=-1: color=black@0,
rotate = angle=30*PI/180: fillcolor=none,
unpremultiply = inplace=1" \
`# 重ねて、拡大した映像も同時作成` \
-filter_complex "[0] [1] overlay = format=rgb,
split [normal] [tmp];
[tmp] crop = y=0: h=40,
scale = width=in_w*6: height=-1: flags=neighbor [zoom]" \
`# ピンク色一色になる?` \
-map "[normal]" -update 1 -frames:v 1 "out5-one-color.png" \
-map "[zoom]" -update 1 -frames:v 1 "out5-one-color-zoom.png"
…良さそう。
本当に?
じゃあちょっと色数を数えてもらおうか。
$ ffmpeg -i "out5-one-color.png" -filter:v "palettegen" -f null "-" 2>&1 | grep "palettegen"
[Parsed_palettegen_0 @ 00000221d9f78b80] 12(+1) colors generated out of 12 colors; ratio=1.000000
12(+1) colors generated out of 12 colors
12 色もあるんですけど!!!!
ただ、そう言われても見た目にはほとんどわからない。
そこで、normalize
フィルターでコントラストをガッと引き伸ばして可視化してみる。
ffmpeg -loglevel warning -y \
-i "out5-one-color.png" \
-filter_complex "normalize,
split [normal] [tmp];
[tmp] crop = y=0: h=40,
scale = width=in_w*6: height=-1: flags=neighbor [zoom]" \
-map "[normal]" -update 1 -frames:v 1 "out6-one-color-normalize.png" \
-map "[zoom]" -update 1 -frames:v 1 "out6-one-color-normalize-zoom.png"
へ~そうなんだ。
じゃあダメだねぇ。
…ということで、unpremultiply
フィルターを使っても、フリンジが完璧な汎用性で解消されるわけではないっぽい。
しかし、私が見つけられた方法の中では現状最もマシでもある。
FFmpeg ともあろうものがこの程度の合成もできないとは到底思えないので、何か真の解決策がありそうな気がする。
募集中。
余談:同色フリンジを肉眼で見る
ピンク色一色の出力を期待した際に観測された同色フリンジ(仮)は、肉眼では到底認識できないレベルの微妙~な色の差だったので、normalize
フィルターのお力を借りてようやく可視化できた。
しかしこの同色フリンジ、場合によっては肉眼で見えることもある。
具体的には、
- 前景に描くものをもっと複雑な形状にする
- 適当にアニメーションさせる
- 拡大する
という感じで頑張れば、ようやく肉眼でも若干見えるようになったり…ならなかったりする。
実際に、ピンク色の背景にピンク色の「あ」という文字が回転している映像を作ってみる。
また、比較用として文字色の方を白にしたものも作成する。
#!/usr/bin/env bash
CreateRotatingText () { # $1: 背景色, $2: 文字色
ffmpeg -loglevel warning -y \
-f lavfi -i "color = color=${1}: size=100x100,
format = rgba" \
`# 透明(黒色透明)の映像ソース` \
-f lavfi -i "nullsrc = size=100x100,
format = rgba" \
`# 透明映像に「あ」を描画して 60°/s で回転させ続け、背景映像に重ね、拡大版も同時作成` \
-filter_complex \
"[1] drawtext = text='あ':
fontfile='C\:/Windows/fonts/NotoSansJP-ExtraBold.ttf':
fontcolor=${2}:
fontsize=90:
x=(main_w-text_w)/2:
y=(main_h-text_h)/2,
rotate = angle=(t*60+30)*PI/180: fillcolor=none,
unpremultiply = inplace=1 [text];
[0] [text] overlay = format=rgb,
split [normal] [tmp];
[tmp] crop = y=0: h=40,
scale = width=in_w*6: height=-1: flags=neighbor [zoom]" \
`# 長さ 6 秒(1 回転分)で無限ループする APNG として出力` \
-map "[normal]" -plays 0 -t 6 "out7-${2}-text-rotate.apng" \
-map "[zoom]" -plays 0 -t 6 "out7-${2}-text-rotate-zoom.apng"
}
CreateRotatingText pink pink # ピンク背景にピンク文字(ほぼ一色だが同色フリンジがある)
CreateRotatingText pink white # ピンク背景に白文字(比較用。普通に字が見える)
計 4 個の APNG ファイルが出力されるが、Qiita でそのまま 4 個の画像を縦に並べるのは見づらいので、1 個の映像に敷き詰めてみる。
また、そもそもだが APNG は Qiita での掲載に難があるし、恐らく元映像には 256 色も使われていない(= GIF アニメに変換しても劣化が無い)ので、GIF アニメにしてみる。
念のために色数も教えてもらう。
gap=10
ffmpeg -i "out7-pink-text-rotate.apng" \
-i "out7-pink-text-rotate-zoom.apng" \
-i "out7-white-text-rotate.apng" \
-i "out7-white-text-rotate-zoom.apng" \
`# 敷き詰めて GIF アニメにする` \
-filter_complex \
"[0] [1] [2] [3] xstack = inputs=4:
layout=0_0|0_h0+${gap}|w0+${gap}_0|0_h0+${gap}+h1+${gap},
split [main] [tmp];
[tmp] palettegen [palette];
[main] [palette] paletteuse" \
-loop 0 -t 6 -y "out7-all.gif" 2>&1 \
| grep "palettegen" # "140(+1) colors generated out of 140 colors" と出る
※「あ」の回転が再生されない場合は、画像をクリックして別タブで開いてみてください。
原寸の 2 個と大きい白文字の 1 個は単に比較用なので、中央の大きいピンク色の映像に注目。
この映像はピンク色一色のはずだが、よ~く見れば大きい白文字の方と同じ形の「あ」のおぼろげな輪郭が見えるはず。
まぁ、そんだけ。
余談の余談:drawtext
のフォント指定がややめんどい
文字を描画するフィルター drawtext
のフォントパス指定パラメーター fontfile
は、クォーテーションやエスケープが意外とめんどかった。
フォント指定を少しでもミスると "Fontconfig error: Cannot load default config file: No such file: (null)" のようなエラーで怒られ続けることになる。
こんな余計な所でハマりたくない人は、下記を全て守るのが無難そうに思う。
- フィルター文字列は全体をダブルクォート
""
- フォントパスは全体をシングルクォート
''
- フォントパスのコロン
:
はシングルエスケープ - パス区切り文字はスラッシュ
/
つまりこう。
"drawtext = fontfile='C\:/path/to/font.ttf':
他のパラメーター,
他のフィルター"
そんだけ。
参考リンク
FFmpeg 公式
- ffmpeg Documentation https://ffmpeg.org/ffmpeg-all.html
- 全部入りの膨大なドキュメント
本記事で使った全ての機能へのリンクを列挙しまくったら長くなったので、畳んでおく。
本記事の重要キャラっぽいやつは何となく太字。
- ffmpeg Documentation https://ffmpeg.org/ffmpeg.html
- 3.1 Filtering: フィルターの概説
-
5.2 Generic options:
-loglevel warning
-
5.4 Main options:
-f
(force format)、-i
(input)、-y
(yes)、-t
(time)、-frames:v
、-filter:v
-
5.11 Advanced options:
-filter_complex
、-map
- FFmpeg Utilities Documentation https://ffmpeg.org/ffmpeg-utils.html
-
2.7 Color: 色書式(
色@不透明度
)、色名
-
2.7 Color: 色書式(
- FFmpeg Formats Documentation https://ffmpeg.org/ffmpeg-formats.html
-
4.15 gif:
-loop
-
4.19 image2:
-update 1
-
4.26 null:
-f null "-"
-
4.15 gif:
- FFmpeg Devices Documentation https://ffmpeg.org/ffmpeg-devices.html
-
3.12 lavfi:
-f lavfi -i
-
3.12 lavfi:
- FFmpeg Filters Documentation https://ffmpeg.org/ffmpeg-filters.html
-
11 Video Filters
-
11.48 crop:
crop
、11.76 drawtext:drawtext
、11.98 format:format = rgba
、11.175 normalize:normalize
、11.180 overlay:overlay
、11.183 pad:pad
、11.184 palettegen:palettegen
、11.185 paletteuse:paletteuse
、11.210 rotate:rotate
、11.212 scale:scale
、11.261 unpremultiply:unpremultiply = inplace=1
、11.285 xstack:xstack
-
11.48 crop:
- 15 Video Sources
-
17 Multimedia Filters
-
17.33 split, asplit:
split
-
17.33 split, asplit:
-
11 Video Filters
- FFmpeg/libavformat/apngenc.c - GitHub
https://github.com/FFmpeg/FFmpeg/blob/n6.0/libavformat/apngenc.c#L296- APNG の
-plays
オプションがまだドキュメントに無い!
- APNG の
ニコラボ
FFmpeg の膨大な機能群を片っ端から日本語で解説してくださっているサイト。
大変参考になった。
Stack Overflow
- FFmpeg transparent PNG black outline issue
https://stackoverflow.com/questions/48144440-
alpha=premultiplied
等が提案されているが、unpremultiply
は無い。
-
- FFmpeg: Removing black outline on overlay after alphamerge
https://stackoverflow.com/questions/71910384- この質問に私が
unpremultiply
を絡めて回答したのだが、その内容を日本語でまとめ直しておきたくて本記事を書いた。
- この質問に私が
- How to extract duration time from ffmpeg output? > 採用済み回答
https://stackoverflow.com/questions/6239350#6239379- FFmpeg のやかましいログは標準エラー出力なので、後の
grep
等にパイプしたければ2>&1
が要る、という話。
- FFmpeg のやかましいログは標準エラー出力なので、後の
他
- コンポジターに必要なアルファチャンネルの知識(前編) - コンポジゴク
http://compojigoku.blog.fc2.com/blog-entry-4.html - コンポジターに必要なアルファチャンネルの知識(後編) - コンポジゴク
http://compojigoku.blog.fc2.com/blog-entry-5.html- プリマルチプライについてのわかりやすい解説。
- Qiita記事でAPNGの投稿と再生はできるのか、実際に上げてみた - Qiita
https://qiita.com/Phroneris/items/ae8430b310b929ec34db- それは私です!
おわり