見出し画像

スキ動画コンテストに参加するはずが、GIF変換ツールを作っていた話

TL;DR

  • スキ動画コンテスト応募のGIF変換で、
    既存ツールに満足できずffmpegを直接叩く
    CLIツールを1本自作した

  • fps・解像度・パレット色数を二分探索
    (解像度優先/fps優先の2戦略並列+
    log-log回帰で高速化)して、サイズ制限内で
    いちばん画質のいい組み合わせを探す設計

  • 同条件でMovie to GIFと比較したところ、
    ファイルサイズはほぼ半分、画質もむしろ
    良好という結果に

  • gifsicle後処理を足したことでスコア関数の
    前提が崩れ、width指数を2乗から1.5乗に
    調整し直すことになった

  • 投稿後にスマホのnoteアプリで高fpsだと
    コマ落ちする問題が発覚し、結局
    13fps・360px固定のプリセットを採用
    (探索の意味を自問するオチ付き)

  • MIT/Apache-2.0デュアルライセンスで公開、
    Pythonのライブラリ/CLIとして使える

想定読者

  • noteのスキ動画やGIFをアップロードする際、
    画質とファイルサイズのバランスに
    悩んでいる人

  • ffmpegでGIF変換を自動化・最適化したい人

  • 二分探索やヒューリスティックな
    スコア設計に興味がある人

  • 個人開発でOSSツールを1本作ってみたい人

  • サイズ制限内でどこまで画質を攻められるか、
    こだわりたい人

  • 本題そっちのけで脱線していく
    個人開発のドタバタを、
    他人事として楽しみたい人

はじめに

第2回スキ動画コンテスト夏の陣」に
応募したときの制作日記は、前回の記事にまとめた。

その中で少しだけ触れたのが、
GIFへの変換作業でこだわりが出て、
専用のCLIツールを1本作ってしまった話。

今回はその顛末を、技術寄りに書いていく。

既存ツールを見て思ったこと

コンテスト公式が紹介していたのは、
キョウスケさんの「Movie to GIF」。

ブラウザ完結する変換ツールで、
使い勝手はすごくよかった。

こちゃの知人が作ったという
iOSアプリ「GIF4n」も
自動調整・スマホ単体で
超手軽にGIF化できるし、

Gifsicle WASMベースの圧縮ツール
(lain2772さん作)も参考にさせてもらった。

ただ、私の普段の執筆フローは
Markdown + 非公式API経由の
アップロードで完結していて、
ブラウザは見た目確認くらいにしか使わない。

なんなら、唯一、自力でのクオリティより
簡単に高品質なアイキャッチが出るという理由で
ブラウザから使い続けていた GPTs ですら、

3日前、Codex スキル版が
リリースされて置き換えができたので、

もはや普段の記事では執筆作業の
すべてがターミナルで完結している。

▼ ずっと愛用していた GPTs はこちら ▼

▼ Codexスキル版「note執事」が ▼
▼ 特典つきで買える記事はこちら ▼

だから、GIFが入る記事のときだけ、
そこにWebツールを挟むこと自体が、
工程としてちょっと浮いてしまう。

そのとき、
キョウスケさんの記事をじっくり読んで

Movie to GIF」が
ffmpeg.wasm を使ってる

と知った。おぉ。なるほど。

だったら、

ブラウザを介さないなら、
ネイティブのffmpegを直接叩けばいい

と気づいたのがきっかけ。

それに、

プリセットから選ぶだけじゃなくて
制限ギリギリまで画質を攻めたい

という欲も出てきた。
だったら自分で作ろう、と。

二分探索で最適化する設計

fps・解像度・色数の組み合わせを二分探索で絞り込んでいくイメージ
fps・解像度・色数の組み合わせを二分探索で絞り込んでいくイメージ

やりたいことは、
fps・解像度(width)・パレット色数の
組み合わせの中から、目標サイズに収まる範囲で
いちばん画質がいいものを探すこと。

fps(6〜30)・解像度・色数(256/192/128)を
総当たりで試すと組み合わせ爆発になるので、
二分探索で絞り込む設計にした。

さらに、

解像度優先
fps優先

の2つの戦略を並列で走らせて、
スコアが良い方を採用する。

スコアはfpswidth色数
重み付けで決まるけど、
この重みの根拠には強弱がある。

特に width の指数は、
動画エンコーディングの
「pixel rate」という概念からの借用で、
後で見直すことになった(詳しくは後述)。

log-log回帰でさらに高速化した話

二分探索そのものも、
最初は「常に中間値を試す」だけだった。

ここで使えたのが、
fps や width を上げていくと
ファイルサイズがべき乗的に
増えていく、という性質。

「log-log」は両対数のこと。
縦軸も横軸も対数目盛りにすると、
このべき乗の関係が
きれいな直線になる。

これを、過去の探索点から両対数グラフに
直線をフィットして次の候補を予測する方式

bisect max regression

日本語でいうなら

回帰二分探索法

と名付けた。嘘だ。
そんなものは一般的に存在しないけど。

まぁ、名前つけておいたほうが
わかりやすいでしょ?

ともあれ、実装したうえで
実データで効果を測ってみると、

探索範囲が実際に予算の壁にぶつかる設定では
ffmpeg実行回数を 10〜20% ほど削減できた。

逆に壁にぶつからない設定では、
特に効果はなかった。

運用中にバグも見つかった。
実際のffmpeg出力は
完全な単調増加ではなく、

近接するフレームレートで
サイズがほぼ同じ(ときには逆転)
になることがある。

すると両対数グラフの
直線の傾きがほぼ0になって、

exp()関数がオーバーフローして
クラッシュしてしまった。

log-log直線の傾きがほぼ水平になり、exp()関数がオーバーフローしてクラッシュした瞬間に驚くMarie
log-log直線の傾きがほぼ水平になり、exp()関数がオーバーフローしてクラッシュした瞬間に驚くMarie

原因さえ分かれば話は単純で、
フォールバックとして

フィットが使えないときは
 素直に中間値に戻す

処理を足して解決した。

余談だけど、

「Movie to GIF」についてる
実測補正付きの予測サイズ表示」も
改良で後から付いた機能なんだって。

同じ課題に向き合うと、
似た発想に行き着くんだなと思った。

実際に使ってみた結果

Movie to GIFと同じ条件で比べてみる

せっかくなので、キョウスケさんの
「Movie to GIF」で実際に書き出したものと、
本ツールで変換したものを
なるべく条件を揃えて並べてみた。

  • Movie to GIF: 540×540 / 30fps →
    17.87MB(実測補正済みの推定通り、
    ぴったりの値で確定)

  • 本ツール(gifsicle込み):
    537×537 / 30fps → 9.88MiB

note本文の画像枠は
20MBまでいけるので、両方そのまま貼る。

Movie to GIFで540×540/30fpsで書き出したバージョン(17.87MB)
Movie to GIFで540×540/30fpsで書き出したバージョン(17.87MB)
本ツールで537×537/30fpsに変換したバージョン(9.88MiB)
本ツールで537×537/30fpsに変換したバージョン(9.88MiB)

ほぼ同じ解像度・fpsなのに、
サイズはこちらの方がほぼ半分

見比べてみると、平坦な背景部分の
色の再現性(ディザリングの荒さ)や、
暗いシーンでのグラデーションの滑らかさも、

サイズが小さいこちらの方が
むしろきれいに出ている。

サイズも画質も両方で有利
という結果になった。

この比較はあくまで、

どっちが優れたツールか

という話じゃない。

Movie to GIFは、
ブラウザだけでトリミングから
変換まで一気にできる。

このお手軽さこそが最大の価値。

私のツールは Python の実行環境が
そもそも必要な時点で、
同じ土俵には立っていない。

画質やサイズの比較は、

ffmpegを直接叩けば、
理論上ここまで攻められる

という参考値くらいに見てほしい。

gifsicle後処理を足したら、探索の勝敗が変わった

gifsicleの後処理でGIFがぎゅっと圧縮される様子
gifsicleの後処理でGIFがぎゅっと圧縮される様子
  • 二分探索は「解像度優先」「fps優先」の
    2戦略の結果をスコアで比較して採用している

  • gifsicleでサイズに余裕ができたことで、
    それまで(width上限640に届かなかった)
    「解像度優先」戦略が上限まで到達できる
    ようになり、スコア上は
    「fps=22 width=640」が
    「fps=30 width=537」を上回って採用された

  • ただし実際に見比べると、fpsが30→22に
    落ちるのは変身シーンのような動きのある
    カットだと結構効く

  • スコア関数のwidthの指数(既定2乗)は、
    動画エンコーディングの「pixel rate」概念
    からの借用で、実はエンコードのコスト
    指標であって知覚的な価値の指標では
    ないと気づいた

  • 指数を1.5まで弱めると、この動画では
    「fps優先」側が僅差で逆転する。
    デフォルトのスコア関数をfps¹×width^1.5に
    調整し、旧来のwidth²版は
    resolution_priority.pyという
    プリセットとして残した

  • fps=10まで落としたバージョンも
    試してみたところ、動きの印象が
    はっきり変わることを確認
    (応募記事側で比較)

--keep-both-strategiesで両方の結果を見比べられるようにした

上記の「どっちの戦略が勝つか」を
実際に目視で確認したかったので、
両方の戦略の結果を保存してスコアを比較表示するオプション
(--keep-both-strategies)も追加した。

投稿後に気づいた誤算:スマホのnoteアプリだと高fpsが逆効果だった

ここまでは、あくまでファイルサイズと
画質のバランスの話。

実は応募記事を投稿したあと、
もうひとつ想定外が見つかった。

PCのブラウザで見る分には、
fpsは高ければ高いほどいい」で
話が終わる。

でも、noteのスマホアプリで確認したら、
高fpsのほうが本来の再生スピードが出ず、
コマ落ちするという壁にぶつかった。

PCではなめらかなのに、スマホの画面ではコマ落ちしていることに気づいて驚くMarie
PCではなめらかなのに、スマホの画面ではコマ落ちしていることに気づいて驚くMarie

このツールが最適化しているのは、
あくまでファイルサイズに対する画質で、
再生環境側の癖までは見ていない。

慌てて応募記事を追記修正して、
解像度とfpsの組み合わせを実機で
片っ端から検証し直すことになった。

537pxのまま高fpsだとコマ落ちする一方、
解像度を360pまで落として13fps前後にすると、
スマホでもヌルヌル動くスイートスポット
見つかった。

結局、応募作品はPC向け(高fps・高解像度)と
スマホ向け(13fps・360px)の
2パターンを併載することにした。

……で、ここでふと我に返った。

二分探索だのlog-log回帰だの、
散々こだわって組み上げたこのツールも、

最終的に採用するのが
13fps・360px固定
プリセットだとしたら、

そもそも探索する意味あった?

凝った二分探索やlog-log回帰のグラフを背負いながら、手元には「13fps/360px」とだけ書かれたシンプルなカードを持って苦笑いするMarie
凝った二分探索やlog-log回帰のグラフを背負いながら、手元には「13fps/360px」とだけ書かれたシンプルなカードを持って苦笑いするMarie

完全にオーバースペック

まぁでも、
PC向けや保存用の高品質GIFなら、

記事本文の20MBまでの枠に
ちゃんと収まるものが増える。

それに、色数の絞り込みや
gifsicleの圧縮のおかげで、

10MBの壁に収まるfps・解像度の
組み合わせ自体も、以前より
広がっているわけで、

きっと無意味ではないはず🤭

追記:あらためて同じ条件で比較しなおした

この記事を読んだキョウスケさんが、
サイズに大きく差が出る原因を
調べて記事にしてくれた。

原因は、パレット生成時の
reserve_transparentの設定。

GIFの差分圧縮に使う
透明色の枠が確保されておらず、
条件によっては毎フレーム
全画面を描き直していた、とのこと。

修正版は2日でリリースされ、
検証記事まで公開されていた。
対応の速さに素直に驚いた。

ただ、比較しなおす前に
ひとつお詫びがある。

前回の比較で出した
こちらの結果(9.88MiB)は、
パレット色数を128色まで
絞ったものだった。

一方のMovie to GIFは
256色で書き出していたので、
実は色数の条件が揃っていなかった。

条件が違う相手と比べて
「ほぼ半分」と書いていたわけで、
ここは素直に反省したい。

というわけで、同じ動画・
同じ解像度(540px)・同じfps(30)・
同じ256色で、あらためて
ffmpegを直接叩いて並べてみた。

  • Movie to GIF(旧版): 17.87MiB

  • Movie to GIF(新版): 14.15MiB

  • Movie to GIF(新版)+gifsicle: 10.27MiB

  • 本ツール(256色、gifsicleなし): 15.51MiB

  • 本ツール(256色、gifsicle込み): 11.17MiB

色数を揃えると、
gifsicleの有無に関わらず
Movie to GIFの方がわずかに小さい。

前回「ほぼ半分」と書いた数字は、
色数を絞った分の差も
乗った状態でのものだった。

条件を揃えたら負けている

Movie to GIFの
お手軽さの代わりに
私はブラウザを介さない分だけ
画質を攻められるはずだった。

そのこだわりどころ
負けているのが、
普通に悔しい。

同じ256色のはずなのに、
なぜこの差がつくのか。

犯人はbayer_scaleだった。

paletteuseのディザリング設定で、
こちらは3
Movie to GIFは5

値を5に揃えて焼き直すと、
gifsicle前後どちらの段階でも
わずかに逆転した。

原因は、判明した。

ただ、二分探索の軸に
これをどう足すかは、また別の話。

fps・解像度・色数の3軸に
bayer_scaleまで足すと、
組み合わせ爆発が
もう一段ひどくなる。

そこまでは、まだ
手をつけられていない。

キョウスケさん、
改良ありがとうございました。

公開しました

MITとApache-2.0のデュアルライセンスで公開した。

CLIとしても、
ライブラリとしてimportして
使うこともできる。

スキ動画コンテストに参加するつもりが、
気づいたらGIF変換ツールを1本作っていた。

目的は私のイラストから
スキ動画をつくること。

そしてそれを note で公開する

ってことのはずだったのに、
今回もまた寄り道してしまった。

おわりに

Threadsでは、日々のことを
もう少し気ままに発信している。
よかったらのぞいてみて。

この記事がよかったら、
スキコメント高評価
応援してもらえると嬉しい。

Marie

いいなと思ったら応援しよう!