BLOG
ブログ

ブログ

画質を測りながらAVIFに変換するツールをAIに書いてもらった

制作・撮影

AVIFという画像形式の話

Webで使う画像といえば長らくJPEGとPNGでしたが、少し前にWebpが加わり、さらにAVIFという選択肢が増えました。
乱暴にまとめると「JPEGの半分くらいのファイルサイズで同じ見た目になる」形式です。
弊社では最近ほとんどWebpでエンコードしていましたが、AVIFでやってもいいんじゃないかと思う機運が静かに広がっている気がします。

対応状況も、Chromeが2020年、Firefoxが2021年、そしてSafariがiOS 16(2022年秋)で対応して、主要ブラウザは一通り表示できるようになっています。まぁ、ちゃんとフォールバック書かないと古いスマホ使い続ける人が見えなくなるけど、5年前のスマホはもうサポート外にしてもいいんじゃないかと。

ということで、品質をプログラムで検証しながら簡単にAVIFを出力してくれるプログラムをClaudeに書いてもらいました。

なぜ「品質を検証しながら」なのか

JPG,Webp,Avifの非可逆圧縮フォーマットでフォーマット変換するとき、品質値(0〜100)を指定します。が、同じ品質値でも画像によって汚いブロックノイズだったり境界線のにじみが発生したりします。空の多い写真は低めでも平気なのに、細かい葉っぱがびっしりの写真は同じ設定だとモヤモヤになる。細かいことを考えずに85とか90とかで圧縮して、テストアップ時に気になる場合に品質値100で圧縮したり、可逆圧縮に変えたりしてみていたのですが、時間がもったいないし、今なら簡単にできそうなので、見た目ベースできれいかどうかを判定しながら出力することを考えました。

見た目ベース、といってもAIの画像認識はトークン馬鹿食いなのに細かいこと得意じゃなさそうなので、人間の見た目に近い品質スコアを機械的に計算できるSSIMULACRA2という指標を使います。Cloudinaryの画像研究者Jon Sneyersが作ったもので、100が元画像と同一、90以上でほぼ見分けがつかない、70以上で高品質という目安。つまり「スコア85を下回らない範囲で一番小さくなる品質値」を探してもらいます。

類似のものがあるというのをどこかで見た気がして調べたら、oavifというターゲット品質エンコーダがありました。ただZigのツールチェーンとlibavifを自前で用意するビルドで、Windows対応は明記なし、入力もPNGのみ。動画向けにはAv1anのターゲット品質モードもありますが、こちらはVapourSynth前提でセットアップがだいぶ重い、ということで車輪の再発明をします。

個人的にRustは嫌いでgoとかC++しか使わないのですが、自分がコードを書かずAIだけが書くならRustはメリットしかないので、Rustでやってもらいました。SSIMULACRA2の計算ライブラリがRust製(rust-av)でそのままリンクできるのも決め手です。

できたものはGitHubで公開しています。
https://github.com/medical-design/guiavif

仕組み

  1. avifencで品質qのAVIFを作る
  2. デコードして元画像とのSSIMULACRA2スコアを計算
  3. スコアが目標(既定85)以上になる最小のqを二分探索
  4. ついでにロスレスWebPも作り、AVIFより小さければそちらを採用

品質値は0〜100の整数なので、二分探索なら7回の試行で確定します。さらに手を入れて、未確定区間に4点を等間隔に置いて同時に試す「多分割探索」にしました。エンコーダのaomは1MP程度の静止画だと12スレッドで頭打ちになる(実測: 0.53MPで6スレッド1.49秒、24スレッド1.46秒)ので、24スレッドを1本に注ぎ込むより、6スレッド×4本を同時に走らせた方が速いというわけです。

この並列化、手元のRyzen 9 5900X(12コア24スレッド)では0.53MPの画像で16.0秒→8.6秒と倍近く効いた一方、スマホ原寸の9.7MP写真では102秒→104秒とまったく効きませんでした。大きい画像はエンコーダが全コアを使い切ってしまうので、分配する余りがない。並列化は画像サイズ次第で効いたり効かなかったりする、という素直な結果です。

お寺の写真で実測

先日外国人だらけの浅草寺のほおずき市で撮ってきた写真2枚(4160×2340、スマホのJPEG)で試しました。

そのままAVIFに圧縮しても元がjpg…なので、スコアは前の画像とどのくらい違うかを出すものなので、jpgのスコアが100になってしまう…

ブログ用に1920pxへ縮小してから変換

1920pxに縮小した無劣化PNGを基準にした、同スコア到達に必要なサイズの直接比較です:

到達スコアJPEGAVIFAVIFの縮み
75以上(そこそこ)品質74・380KB222KB-42%
85以上(既定)品質91・648KB381KB-41%
92以上(ほぼ無劣化)品質100・1,742KB918KB-47%

スコアの探索をするために何度もエンコード繰り返すので、1枚エンコードするのに十数秒かかる場合もあるけど、きれいに出るから許してちょんまげ。

使い方

GUI版はウィンドウに画像をドロップするだけ。変換が終わると同じフォルダに.avifができます。

CLI版はAIエージェントやバッチ処理から使う想定で、結果をJSONで返します。

$ avifq photo.jpg --target 90
$ avifq *.jpg --json --quiet   # 結果をJSON Linesで標準出力へ

何も考えずにClaudeにこのプログラムに投げ込んでもらえて便利ぃい!とか考えて作ったけど、ほとんどの場合、画像を所定の場所に置くのは人の手でないとできないので、それほど活躍の場面なかった。忘れて欲しい。

社内のほとんどがMacなので、Mac用のも作ってもらいましたが、実際試してません。(macはbrew install libavifとcargo buildが必要)。詳しくはリポジトリのREADMEへ。

おわりに

Rustのコードは全体で1,000行ちょっとありますが、私自身は1行も書いていません。仕様を伝えて、ベンチマークを取らせて、遅かった設計案(スコア計算を後回しにする案は逆に遅くなりました)を実測ベースで捨てさせて、という進め方で作業時間は2時間ほど。

Fableが使える期間が2回延長されたけど、7/20までしか使えないので駆け込みでやっておきたいこと探してます。