スキ動画コンテストに参加するはずが、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・解像度(width)・パレット色数の
組み合わせの中から、目標サイズに収まる範囲で
いちばん画質がいいものを探すこと。
fps(6〜30)・解像度・色数(256/192/128)を
総当たりで試すと組み合わせ爆発になるので、
二分探索で絞り込む設計にした。
さらに、
「解像度優先」
「fps優先」
の2つの戦略を並列で走らせて、
スコアが良い方を採用する。
スコアはfps・width・色数の
重み付けで決まるけど、
この重みの根拠には強弱がある。
特に width の指数は、
動画エンコーディングの
「pixel rate」という概念からの借用で、
後で見直すことになった(詳しくは後述)。
log-log回帰でさらに高速化した話
二分探索そのものも、
最初は「常に中間値を試す」だけだった。
ここで使えたのが、
fps や width を上げていくと
ファイルサイズがべき乗的に
増えていく、という性質。
「log-log」は両対数のこと。
縦軸も横軸も対数目盛りにすると、
このべき乗の関係が
きれいな直線になる。
これを、過去の探索点から両対数グラフに
直線をフィットして次の候補を予測する方式
bisect max regression
日本語でいうなら
回帰二分探索法
と名付けた。嘘だ。
そんなものは一般的に存在しないけど。
まぁ、名前つけておいたほうが
わかりやすいでしょ?
ともあれ、実装したうえで
実データで効果を測ってみると、
探索範囲が実際に予算の壁にぶつかる設定では
ffmpeg実行回数を 10〜20% ほど削減できた。
逆に壁にぶつからない設定では、
特に効果はなかった。
運用中にバグも見つかった。
実際のffmpeg出力は
完全な単調増加ではなく、
近接するフレームレートで
サイズがほぼ同じ(ときには逆転)
になることがある。
すると両対数グラフの
直線の傾きがほぼ0になって、
exp()関数がオーバーフローして
クラッシュしてしまった。

原因さえ分かれば話は単純で、
フォールバックとして
「フィットが使えないときは
素直に中間値に戻す」
処理を足して解決した。
余談だけど、
「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までいけるので、両方そのまま貼る。


ほぼ同じ解像度・fpsなのに、
サイズはこちらの方がほぼ半分。
見比べてみると、平坦な背景部分の
色の再現性(ディザリングの荒さ)や、
暗いシーンでのグラデーションの滑らかさも、
サイズが小さいこちらの方が
むしろきれいに出ている。
サイズも画質も両方で有利、
という結果になった。
この比較はあくまで、
「どっちが優れたツールか」
という話じゃない。
Movie to GIFは、
ブラウザだけでトリミングから
変換まで一気にできる。
このお手軽さこそが最大の価値。
私のツールは Python の実行環境が
そもそも必要な時点で、
同じ土俵には立っていない。
画質やサイズの比較は、
「ffmpegを直接叩けば、
理論上ここまで攻められる」
という参考値くらいに見てほしい。
gifsicle後処理を足したら、探索の勝敗が変わった

二分探索は「解像度優先」「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のほうが本来の再生スピードが出ず、
コマ落ちするという壁にぶつかった。

このツールが最適化しているのは、
あくまでファイルサイズに対する画質で、
再生環境側の癖までは見ていない。
慌てて応募記事を追記修正して、
解像度とfpsの組み合わせを実機で
片っ端から検証し直すことになった。
537pxのまま高fpsだとコマ落ちする一方、
解像度を360pまで落として13fps前後にすると、
スマホでもヌルヌル動くスイートスポットが
見つかった。
結局、応募作品はPC向け(高fps・高解像度)と
スマホ向け(13fps・360px)の
2パターンを併載することにした。
……で、ここでふと我に返った。
二分探索だのlog-log回帰だの、
散々こだわって組み上げたこのツールも、
最終的に採用するのが
13fps・360px固定の
プリセットだとしたら、
そもそも探索する意味あった?

完全にオーバースペック。
まぁでも、
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
