動画を「真っ黒かつ無音」部分に基づいて分割な ffmpeg

シーン検出のうちのより限定的なやつ。

動画を「真っ黒かつ無音」部分に基づいて分割な ffmpeg

動機

「動機」は見出しが示してる、とも言えるんだけれど一応。

話としては最初に言った通り「シーン検出」のカテゴリに包含出来る内容なのだけれど、そのなかでも「フェードイン/フェードアウト」でシーンの切れ目を表現しているような動画で、その位置を区切りとして千切りたい、てことね。

なので今回ここでやることはffmpeg でシーン検出の方を発展させることにも繋がる、てことな。

前提と前置き

ffmpeg でシーン検出で示したのと同じく、今回の目的に使えるフィルタがある。blackdetectsilencedetect ね。

で、シーン検出の方もそうなんだけれど、「検出出来るからといって」、目的のことそのものをやってくれるものはない、と思う。silenceremove なんてのはあるんだけどね、ないわけだ。なので「フィルタがレポートしてくれた情報に基づいてがむばる」しかないてことな。

あと、今回やるのは、その「フィルタがレポートしてくれた情報に基づいてがむばるスクリプト」てことになるんだけれど、ffmpeg に限らずこの手の「自動化を夢見る」てのは、得てして「目で、手でやっちゃった方が早い」ことになりがちなのは注意ね。

今回のネタの分割ポイント探しについては、「目視で探す」場合に問題となりうるのが「秒未満の指定精度」なのだけれど、これだって「コマ送り」が自由に出来るビューワを使えば結構なんとかなる。ffplay なら「s」で、Windows Media Player Classic なら「Ctrl →」「Ctrl ←」でコマ送り出来るので、「コマ数 / fps」で秒未満位置を特定出来る。

もうひとつ「自動化を夢見る」際に問題になるのが「パラメータ微調整の試行錯誤時間」ね。今回のネタでもこれが顕著で、そもそも「分割したくなるような動画」なわけだから、つまりはおそらく「かなりのデカい動画」なわけであろう、であれば検出フィルタを動かす時間は特に映像の方は常にかなりの時間がかかるし、「検出後の実際の分割処理」も都度試すとなれば、本当に膨大な時間がかかる。

なので必要な「心意気」はこれは何かつーと、「適宜諦めて手動調整モードに頭を切り替える」ことね。全自動にはこだわり過ぎないこと。よろしい?

blackdetect と silencedetect そのものについての基礎的な話

公式ドキュメントはいつも通り「バカで不親切でバカでなおかつバカ」だが、これについては「良く探せば」書いてある:

音声のみ処理したいとして
1 [me@host: ~]$ ffmpeg -i target.mp4 \
2 > -af "silencedetect,ametadata=mode=print:file=ametadata.txt" \
3 > -vn -f null -
映像のみ処理したいとして
1 [me@host: ~]$ ffmpeg -i target.mp4 \
2 > -vf "blackdetect,metadata=mode=print:file=metadata.txt" \
3 > -an -f null -
音声・映像両方処理したいとして
1 [me@host: ~]$ ffmpeg -i target.mp4 \
2 > -af "silencedetect,ametadata=mode=print:file=ametadata.txt" \
3 > -vf "blackdetect,metadata=mode=print:file=metadata.txt" \
4 > -f null -

(ametadatametadataフィルタの file オプションを使うことで)ファイルに吐き出さなくてもコンソールにはレポートされる:

が、今回やりたいことのようにスクリプトから使うにはファイルに吐いてもらう方がいかろう。ファイルに吐いてもらった場合はこんな:

metadata
1 frame:0    pts:0       pts_time:0
2 lavfi.black_start=0
3 frame:30   pts:89999   pts_time:0.999989
4 lavfi.black_end=0.999989
5 frame:676  pts:2027999 pts_time:22.5333
6 lavfi.black_start=22.5333
7 frame:722  pts:2165999 pts_time:24.0667
8 lavfi.black_end=24.0667
ametadata
 1 frame:34   pts:34816   pts_time:0.789478
 2 lavfi.silence_start=0
 3 frame:59   pts:60416   pts_time:1.36998
 4 lavfi.silence_end=1.38197
 5 lavfi.silence_duration=1.38197
 6 frame:197  pts:201728  pts_time:4.57433
 7 lavfi.silence_start=3.78222
 8 frame:925  pts:947200  pts_time:21.4785
 9 lavfi.silence_end=21.4859
10 lavfi.silence_duration=17.7036
11 frame:963  pts:986112  pts_time:22.3608
12 lavfi.silence_start=21.5744
13 frame:1036 pts:1060864 pts_time:24.0559
14 lavfi.silence_end=24.0643
15 lavfi.silence_duration=2.48986

blackdetect、silencedetect が受け容れるオプションが出来るのは「どの程度静か?」「どんくらい黒すけ?」「どんだけそれが続いたらオケ?」てこと。blackdetect なんぞは汎用的に書きゃぁ「黒以外」にも拡張できるはずなんだけどね、ffmpeg フィルタの作者たちはだいたい概ね「阿呆」なのでそんな高等なことは出来ない。ゆえ、blackdetect では「黒の程度(pix_th)」と「黒が占める率(pic_th)」と「継続時間の閾値(d)」、silencedetect では「無音の程度(n)」と「継続時間の閾値(d)」ね。

「パラメータ微調整」で問題になりうるのが、映像の方だとまさに「ウォーターマーク」(透かし)かもしらんねと思う。これを removelogo や delogo で取り除けるのであれば、案外 pix_th=0.0pic_th の大きめの値、で問題なくなる。そう出来ない場合調整が大変だろうと思う。特に「白文字での字幕」も含まれてるとなおさら。

音声の方は実際もっと面倒。デシベルか割合で指定するんだけれど、ターゲットの動画によって結構違うわけよ。ある動画で例えば「n=0.0000306」でうまく見つけてくれたのに、これを別の動画に適用すると一つも無音部分が見つからない、なんてことが良く起こる。当然人間の耳にはその差はまったく識別出来ない。音声に関しては特に、ちょっと荒っぽい値にしといた方が無難と思う(つまり少し大きめの n を与えておく)。

ゆえ、実際「ffmpeg に検出させてそれを自前スクリプトが読み込んで云々」なアプローチを採る場合は、ffmpeg には粗っぽく、かつ多めに検出させておいて、自前スクリプト側で刈り込んだ方が多分楽。(「多めに」は継続時間(d)を小さくするてこと。)

スクリプト化のための発想

2つの検出器が見つけた範囲のオーバラップを切れ目位置にする、てことだわな。そりゃそーでしょ「真っ黒かつ無音」なのだもの。てのは当然の前提として。

もう一つが「結局手動調整はしたくなるべ」に備えるには、「スクリプトが直接分割処理までまかなう」のは「大きなお世話」でしかなくて。なので「スクリプトがスクリプト(bash スクリプト)を吐き出す」。

これ以外の本質としては、ffmpeg で動画分割(とか)で昨日追記した部分(「2019-02-19追記: 動画の後ろの方を部分抽出」な見出しのやつ)で。つまりスクリプトが吐き出す結果は例えばこんな:

 1 #! /bin/sh
 2 ffmpeg="c:/Program Files/ffmpeg-4.1-win64-shared/bin/ffmpeg"
 3 inmov="./#04.mp4"
 4 outbase="../#04"
 5 ext="mp4"
 6 
 7 # 00:00:00.000 -> 00:03:03.483
 8 "${ffmpeg}" -y \
 9      -i "${inmov}" \
10      -vf "removelogo=../__auto.png,fillborders=right=6:mode=fixed" \
11      -to 00:03:03.483 "${outbase}-01.${ext}"
12 
13 # 00:03:03.483 -> 00:03:34.933
14 "${ffmpeg}" -y \
15      -ss 00:02:58.483 -i "${inmov}" \
16      -vf "removelogo=../__auto.png,fillborders=right=6:mode=fixed" \
17      -ss 00:00:05.000 -to 00:00:36.450 "${outbase}-02.${ext}"

先の「大きなお世話」と同じで、「一気に全部」やろうとすると「部分的に手動調整」しにくくなるのよね、なので「個々の範囲抽出は各々独立した ffmpeg 呼び出しにする」。(このためにffmpeg で動画分割(とか)での追記が効く。)

完成品のゴミスクリプト

「まともな」ものに仕立て上げるつもりもあまりないもんで、なので GIST に置いてみた:

使い方はたとえばこんなね:

 1 [me@host: ~]$ python video_split_by_blank.py \
 2 > ./target.mp4 \
 3 > --ou=../target \
 4 > --vf_prep="removelogo=logo.png,fillborders=right=6:mode=fixed" \
 5 > --ffmpeg="c:/Program Files/ffmpeg-4.1-win64-shared/bin/ffmpeg" \
 6 > --sd='{"d": 0.08, "n": 0.0001}' \
 7 > --bdet_pa='{"d": 0.08, "pic_th": 0.985, "pix_th": 0.0}' \
 8 > --uns='[[0, 30.0], ["00:25:57", "00:27:49"]]' \
 9 > --dur=0.05 > spl.sh
10 [me@host: ~]$ sh ./spl.sh

sdet_paras, bdet_paras の調整がまぁ要するに面倒というか繊細なわけなんだけれど、上で説明したように「ここは粗っぽく、多めに」となるようにしておいて、その代わりに unsplittablesduration_threshold で刈り込むとサクっと使えると思う。

あ、あとコードを読めばわかると思うけど、「検出器呼び出し」そのものはスクリプトが直接呼び出しちゃう。そしてその結果のキャッシュもしてる。ので、使う際の注意は、「検出器呼び出し」の処理中は「q」で止めないことてのが一つと、もう一つが、「キャッシュしてる」のが理由で二度目以降びっくりするかもしれないてこと。キャッシュにあれば検出器のための ffmpeg 呼び出しをしないので、あっという間に処理が終わるしコンソールも静か。注意点はこの二つだけかな。

22日05:20追記: Revision 11 での変更について

最初に挙げてから少しだけ変えたんだけれど、この「Revision 11 での変更」の意図だけはちゃんと説明しないといけない内容なので。

unsplittables てのは要するに「ここでは切らないでくれぃ」な制御なんだけれど、「ここでは切りたくない」には実際二種類あるわけなのよ。

つまり、blackdetect と silencedetect が検出した結果「01:10:10~01:10:15」が無音で黒、だとする場合に、「ここでは切りたくない」のが、果たして「この範囲のどこでも切って欲しくない」なのか、「切っては欲しいがそこじゃない」なのか、てこと。切れ目はプログラムとしては「「(01:10:10 + 01:10:15) / 2 = 01:10:12.5」」で切るようになっているのだけれど、detector の閾値調整によっては、「あんまし黒でなくてあんまし無音でない遷移状態な中途半端位置」を指してしまう場合がある。つまり「そこじゃなくてもっと先」とか「もっと前」が目的の切れ目位置である場合がある、てこと。

Revision 11 での変更はこれに対応するためのもの。なのだが、「そもそもどこでも切って欲しくない」向けにはちょっと弱ったことになって。unsplittables での指示を少し広めに与えなくちゃいけなくなるのだわ。まぁでもこれは致し方ないと諦めてくれぃ。

2019-03-07追記: 「「真っ黒かつ無音」部分に基づいてチャプターマーク」(Rev43)

実際の分割プロセスを動かし始めちゃうと、かなりの時間がかかるわけね。たとえば1時間半の動画で3時間かかるとか例えばそんな。(詳細に測ったことがないので正確なところは言わない。けど感覚としてはそれほど膨大な時間てこと。)なので「切れ目がいいあんばいなのか」の素早い確認手段が欲しいわけなのよ。ので、これのオマケとして書いたチャプターマーク挿入を、今回の「真っ黒かつ無音に基いて」でやっときたいなと。

やることははっきりいってほとんど同じなので、同じプログラム(Revision 43)で出来るようにしといた。今のところ「分割するかチャプターマークか」の二択。そのうち「分割しつつチャプターマークも入れる」もやるかもしれんけど、今のところはここまで。

2019-03-10追記: つけちゃったチャプターマークを消すのがめんどい

めんどいというか、正解にたどり着くまでにエラく迷走してしまった。迷走の原因はこいつ。正解はこっち

不正解、と言ってるのはこれ:

1 [me@host: ~]$ ffmpeg -y -i input.mp4 -c copy -fflags +bitexact -map_metadata -1 output.mp4

「完全無欠の誤り」というもんでもないのが悩ましくて。これね、「チャプターについた情報」は消える。要するに「チャプター2 – 始まりと終わりのプロローグ」みたいな「情報」(タイトル)だけを消しこみにいって、チャプターそのものは消えない。

正解の、「チャプターマークそのものを消す」のには

1 [me@host: ~]$ ffmpeg -y -i input.mp4 -c copy -map_chapters -1 output.mp4

とする。

正解にたどり着くまで 1時間は彷徨ってたわ。

2019-03-12追記: Makoナイズ (Rev76現在)

「黒かつ無音部分に基いて云々」の「云々」で欲が出てきてしまったのだが、「云々」はこのスクリプトにとっては所詮「出力スクリプトのバリエーション」に過ぎず、データ収集等々はいつでもおんなじ。ゆえ、「テンプレートエンジン」に頼ろう、と。

採用したのは Mako。Jinja2 の方が普及度は高いと思うので、Jinja2 にするか迷ったけど、Mako の方が書きやすい気がしたので Mako で。普及度が相対的にみれば Jinja2 より低いだろうといったって Reddit が使っているのだもの、十分ビッグプロジェクトだわよ。

Rev76 時点では単にこれまで書いてきたヤツを Mako 化しただけ。けどもちろん「自作テンプレートを使える」版こそが望みのもの。自分が欲しいんで、このあと書くよ。(それを使ってすぐにでもやりたいのは「黒かつ無音部分に基いて背景画像を変える」てヤツ。)


2019-03-12 12:45追記: 自作テンプレートを「--render_templatefile--render_template」オプションで指定できるようにして、なおかつ「黒かつ無音部分に基いて背景画像を変える」の例、書いた

2020-10-18 ffchopreview.py

ここでやってる「作業」に限らないんだけれど、こういう「分割だの結合」の手軽なプレビューがあるのとないのとでは、精神衛生的にかな~り違ってな。まぁこういうのは結局は「ちゃんとした GUI」が一番ストレスフリーではあるものの、「ffmpeg しか持ってないんぢゃわりゃぁ」な場合でもお手軽りたいと ffchopreview.py なんてなのを作った。

「動画を「真っ黒かつ無音」部分に基づいて分割な ffmpeg」が目的の作業の場合で、なおかつ video_split_by_blank.py を使ってるなら、紹介済の「チャプターマークをつけてみる」だけでも結構楽にはなるんだけれど、特に video_split_by_blank.py がパラメータ微調整がうまくいかずに半手作業に陥ることも多いので、そうしたケースでも ffchopreview.py は重宝するかもしらんねと思う。