LoginSignup
9
8

FFmpegで重ねた物にフリンジが出る場合はunpremultiplyフィルターで解決…なのか?

Last updated at Posted at 2023-05-06

前提

コマンドラインで 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"

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"

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"

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"

out1-alpha-cyan.png
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"

out2-pre.png
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"

out3-unpre.png
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"

out4-unpre-pre.png
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"

out5-one-color.png
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"

out6-one-color-normalize.png
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" と出る

out7-all.gif

※「あ」の回転が再生されない場合は、画像をクリックして別タブで開いてみてください。

原寸の 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 の膨大な機能群を片っ端から日本語で解説してくださっているサイト。
大変参考になった。

Stack Overflow

おわり

9
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8