Chienomi

Misskeyで画像を圧縮してドライブや帯域を節約

Live With Linux::practical

MisskeyではドライブというMisskey上のストレージがあり、任意のファイルをこのドライブにアップロードしてノートに添付することができる。

ただし、ドライブには容量制限が設けられており1、その範囲内での利用となる。 この関係で、スクリーンショットなどの投稿しすぎるとドライブの容量を使いし潰してしまい、それ以上画像投稿ができない事態も考えられる。

ドライブにあるファイルは何度でもノートに添付することができるが、ドライブ上からファイルを削除するとそれを添付したノートもろとも消える。 フォルダに消えてしまっても構わないノートのためのファイルをまとめておいて、容量が減ってきたら削除することもできるが、ファイルサイズは小さいほうが有効だ。

ここでは、スクリーンショットや写真などの画像の圧縮について解説する。 Linux環境を想定して説明するが、同様の知識を活用して他のプラットフォームでも応用することはできるだろう。

基礎知識

カメラが高性能化した昨今、写真はかなりのサイズになってしまう。 そのまま投稿すると、単にドライブの容量を圧迫するだけでなく、それを閲覧する人にも負担になってしまうだろう。

スクリーンショットに関しても、最近の環境だと大きくなりやすい。 場合によっては数MBから十数MBクラスとなってしまう。

いずれにせよ、そのまま投稿するのではなく、圧縮して投稿ーするのが望ましい。 そして、それは投稿しようとするものに合わせた最適なメソッドを選択することが必要だ。

前提として、Misskeyには「オリジナル画像を保持」オプションが存在しているが、このオプションによらずJPEG画像として画像は圧縮される。 このオプションはドライブに置かれるファイルが圧縮されたものが置かれるかどうかという違いである。

つまり、このオプションを有効にしない場合は、ある程度は圧縮された状態になるため、巨大なファイルがそのまま圧迫するわけではないが、それでも限定的な話になるし、意図には沿わないため、自分で圧縮した上で「オリジナル画像を保持」オプションを有効にしてアップロードしたほうが容量削減になる。

投稿時はそのアクションによらず、圧縮された状態での表示になる。 もし投稿したドライブ上のファイルをそのまま見せたい場合は、ファイルのメニューから「ダウンロード」で得られるURLを共有する必要がある。

また、投稿時のデータはWebPであってもAVIFであっても「見られない」という人はいなかったため、これらのフォーマットに変換しての投稿はかなり効果的だ。

圧縮メソッド

写真のケース (JPEG)

写真の場合、そもそもキャンバスサイズ自体が非常に大きいが、SNSの共有としてはそこまでのサイズは必要ないだろう。 リサイズすれば相当サイズは小さくなるので、まずリサイズするところからスタートすることを考えたい。

また、写真でEXIF情報を消したいケースが多いかもしれないので、その点を含めて考える。 これ自体は単純には

convert -resize 1000x1000 -strip in.jpg out.jpg

のようにすれば、最大辺長1000pxとし、EXIF情報を削除できる。

これで満足できれば良いが、jpegoptimを使ってさらに削減が可能だ。 jpegoptimによる変換は視覚的な影響が小さく、有効でない場合もあるが、うまくいけば大きくサイズを小さくできる。

別のメソッドとして、異なる画像形式に変換するという方法もある。 有力なのはWebP, AVIF, JPEG XLだろう。

JPEG XLはMisskeyが画像として認識しないため機能しない。 このため、今回の主題からは除外する。

WebPであってもAVIFであっても、それで投稿すればかなりのデータ削減が見込める。 JPEGでqualityを下げるのと比べればこれらのquality下げは「見られる」状態を留められるので、美しい状態を目指すのではなく、あくまで共有を目的としたものであれば、quality調整により著しくサイズ削減を行うことも可能だ。

スクリーンショットのケース (PNG)

スクリーンショットもサイズが大きいが、必ずしもリサイズが可能とは限らない。 また、「なんとなくわかれば良し」の場合はいいのだが、文字が読める必要があるとなるとロッシーフォーマット採用も厳しいことがある。

スクリーンショットの場合使用されている色数は相当限定されるため、pngquantによる圧縮が非常に効果的である。 pngquantは視覚的な劣化をとても抑えた上で大きな圧縮を行うことができるため、pngスクリーンショットに関してはpngquantを通すのが最も有力。下手にJPEGにするよりよっぽど効く。

別の方法として、WebPやAVIFのロスレス圧縮を行うという方法もある。 ただ、ロスレス圧縮は削れる情報が限られているから、pngquantと比べて著しい成果をあげられるかは疑問だ。

雰囲気わかれば良しであれば、WebPやAVIFによるロッシー圧縮も考えられるだろう。

サイズ比較

次に示すのは、4000x3000ピクセルの写真データをベースとし、最大辺1000ピクセルに絞ってEXIF情報を消した(-resize 1000x1000 -strip)あと、それぞれのメソッドで圧縮した結果である。

-rwxr----- 1 haruka haruka 7039084  1月  2 20:42 photo.jpg
-rw-r--r-- 1 haruka haruka   91270  2月 11 22:31 photo-avif.avif
-rw-r--r-- 1 haruka haruka   19542  2月 11 22:35 photo-avif-60.avif
-rw-r--r-- 1 haruka haruka  580431  2月 11 22:30 photo-resize.jpg
-rw-r--r-- 1 haruka haruka  104495  2月 11 22:30 photo-resize-optim.jpg
-rw-r--r-- 1 haruka haruka   88322  2月 11 22:31 photo-webp.webp

表にすると以下のとおり

アルゴリズム q サイズ
JPEG (Original) - 7039084 1.0000
ImageMagick (resize) - 580431 0.0824
jpegoptim max=70 104495 0.0148
WebP 0 100 88322 0.0125
AVIF default 91270 0.0129
AVIF 40-52 19542 0.0027

次に示すのは、FHDサイズの画面のスクリーンショットと、pngquant, WebPロスレス, WebPロッシー, AVIFロスレス, AVIFロッシー(q=default), による圧縮、JPEGへの圧縮(q=95)である。

-rw-r--r-- 1 haruka haruka 1610518  2月 11 21:34 screenshot.png
-rw-r--r-- 1 haruka haruka  211153  2月 11 21:45 screenshot-avif.avif
-rw-r--r-- 1 haruka haruka 1427757  2月 11 21:51 screenshot-avif-lossless.avif
-rw-r--r-- 1 haruka haruka 1464228  2月 11 21:41 screenshot-jpeg.jpeg
-rw-r--r-- 1 haruka haruka  494409  2月 11 21:34 screenshot-quant.png
-rw-r--r-- 1 haruka haruka  275200  2月 11 22:06 screenshot-webp.webp
-rw-r--r-- 1 haruka haruka 1191394  2月 11 22:06 screenshot-webp-lossless.webp

表にすると以下の通り:

アルゴリズム q サイズ
PNG (original) - 1610518 1.000
PNG (Quant) 65-80 494409 0.306
JPEG 95 1464228 0.909
WebP 0 100 275200 0.170
Webp lossless 1191394 0.739
AVIF default 211153 0.131
AVIF lossless 1427757 0.886

解説と結論

写真について

ソースは夜に車の写真を撮ったものだったのだが、--max 70なopegoptimが相当効いている。 jpegoptimはクオリティファクターなしだとほとんど圧縮されないため、視覚的影響を考慮して--max 70から--max 80あたりを探ると割と満足な結果が出そうである。

ただ、そもそもImageMagickを使ってリサイズした時点で画像の劣化が割と明確に出ている。 荒れているというよりも、コントラストが少し損なわれた。

cwebpavifencによるデフォルト圧縮はいずれもjpegoptim以上の効果を発揮した。 jpegoptimのパラメータは--max 70であるため、拡大すれば劣化は明確に視認可能だ。 WebPおよびAVIFの画像はこのjpegoptimよりも良好な品質であり、サイズも少し小さいため、可能ならこれらのツールにより圧縮するほうが良いだろう。

AVIFとWebP画像の優劣は決めがたい。だいたい同じようなクオリティであり、サイズはWebPのほうがわずかに小さかった。 GdkpixbufがWebPに対応していないこともあり、WebPのほうが取り回しは悪いが、あくまでMisskeyへの投稿用の一時的な画像なら問題はないはずだ。

そして、1点、AVIFとWebPに大きな違いが出る部分がある。 それが処理速度だ。VP8ベースのWebPのほうがAV1ベースのAVIFよりもずっと速く、AVIFの生成はそれなりに時間がかかる。 実用的な問題を考えればWebPに圧倒的に軍配が上がる。

cwebpにはリサイズ機能があるが、長辺制限がないこと(オートレシオはある)と、cwebpでリサイズするより、ImageMagickでリサイズしてからcwebpで圧縮したほうが視覚品質が良好であったことから、もう1段階踏んだほうが良い結果にはなるだろう。

以上を踏まえると、投稿写真を作る処理は次のようなものが良いと考える。2

#!/bin/zsh
cp "$1" ~/tmp
cd ~/tmp
mogrify -resize "$2x$2" -strip "$1"
cwebp -preset photo "$1" -o "#{1:r}.webp"

本当にざっくり雰囲気や何を写しているかわかればいい場合は、AVIFでq値高めに設定すると驚異的なサイズまで小さくできる。 こういうケースは多いので有用そうなのだが、たいていそういうのは「スマホで撮ってそのままあげたい」なので、PCに写して圧縮」がめんどくさいのが難点。残しそうにないやつなら、おとなしくそのままupして、フォルダ分けしておいて後で消すほうがずっと有効だろう。

スクリーンショットについて

スクリーンショットは性質によって少し事情が違う。 つまり、文字があるなどの理由でロスレスにしたいかどうかだ。

ロスレスにしたい場合、AVIFでもWebPでも効果は結構微妙。だが、強調したいのはここでテストした限り、JPEG以外は気にする要素はほとんどないということだ。 例えばあなたがイラストレーターであり、最高品質の画像をそのまま見てほしいということであればロスレスが良いだろうが、それは共有時にアドレスを共有するという手間まで含めた話になる。

pngquantは非常に効果が高く、フォーマットもPNGが維持されるため理想的だが、WebPやAVIFで圧縮したほうが圧縮率が明らかに優れているため、手間をかけるつもりであればこちらもWebPかAVIFにしたほうが良さそうだ。 q factor調整が楽なので、汎用性がある手順はAVIFだが、WebPが早いという利点も捨てがたい。

個人的にはAVIFのほうが良さそうに思えたため、次の手順を考える。 speed factorはもっと考える余地があるが、速度を考えるなら単純にcwebp -preset pictureを選択するほうが良さそう。

#!/bin/zsh
cp "$1" ~/tmp
cd ~/tmp
avifenc -s 0 "$1" "{1:r}.avif"

Misskeyにおけるベストプラクティス

教えていただいた内容と、自分なりに確認した内容だが、Misskeyの画像処理の動作としては

  1. アップロードされたファイルが画像であり
  2. PNG/JPEGフォーマットでない、またはメタデータが存在する場合
  3. 配信用の画像データを生成する

ということのようだ。 別の見方をすれば、PNGまたはJPEGフォーマットであり、メタデータが存在しないのであればそれはそのまま保存される。

通常の画像添付は生成された画像が使われる(生成されていない場合はそのまま使われる)。 「オリジナル画像を保持」オプションは、画像生成が行われた場合に、ドライブに置かれるファイルが生成された画像で置き換わらないようにするものであるようだ。

ドライブ容量の消費はこの生成された画像を含まず、ドライブに置かれているファイルサイズのみからなる。 このため、ドライブ容量だけの観点から言えば、AVIFによって容量を圧縮するのは有効だが、みんなの帯域消費を減らす、サーバーの負担を減らすといった観点からだと良いとは言えない。

ベストなのは、PNG/JPEGのフォーマットのまま、サイズを削減し、メタデータを除去してアップロードすることだ。

以下ではコマンドラインツールであるImageMagick, jpegoptim, pngquantを使って画像を圧縮する例を示す。 ここで示す例はすべて元の画像を置き換えるため、処理する画像はコピーにしたほうが良い。

まず、サイズを小さくするとともにメタデータをstripする。 -resizeの値は両辺がこの値よりも小さくなるように調整される。

mogrify -resize 1000x1000 -strip foofile.png

JPEGの場合はjpegoptimを使う。

jpegoptim --max=80 foofile.jpg

PNGの場合はpngquantを使う。

pngquant -f -s 1 -Q 65-80 --strip -- foofile.png

ではサイズとq値についてである。

基本的に写真などでもサイズは1000pxで十分なサイズである。 拡大されることを予期するものであれば、予め拡大された注目される部分を切り取ったソースを用意してやったほうが良い。

これより大きなサイズの画像を必要とするのは、イラストレーターや写真家が作品として自身の画像を見せたい場合だろう。 その場合は、配信で適切なサイズは問題なく判断できるはずだ。

jpegoptimの--maxの値は、およそ最小値70、最大値80あたりで考えれば良い。 ほとんどの場合、80で満足できるだろう。

pngquantの-Q値は、スクリーンショットなどで比較的色数が多い場合は65-80あたりが良い。 一方、色数が少ないソースの場合はこれだと圧縮できないことがあり、最低では40-65あたりが考えられる。 ほぼ単色のソースなどは、37-42という低く狭いレンジで指定することもある。

およそ、ソースと出力したいクオリティに合わせて、40-65, 50-70, 65-80あたりを使い分けると良い結果になる。

このようにしてサイズが小さく、メタデータのない画像データをアップロードすることで、ドライブも、ユーザーの帯域も、サーバーの負担も低減することができる。 これはActivityPubや他のインスタンスにとっても良いことだ。

おまけ: JPEG XL

だいぶ時期尚早な感じになってしまうが、JPEG XLによる画像品質はどうだろうか。

写真
アルゴリズム q サイズ
JPEG (Original) - 7039084 1.0000
ImageMagick (resize) - 580431 0.0824
JXL lossless 522208 0.0741
JXL 75 80083 0.0113
スクリーンショット
アルゴリズム q サイズ
PNG (original) - 1610518 1.000
JXL lossless 971098 0.602
JXL 90 498781 0.309
JXL 80 368341 0.228
JXL 70 299773 0.186

JPEG XLファイルに関してはGdkpixbuf上でのデコードが重い。 写真ではそこまで気にならないが、スクリーンショットは相当重かった。

ソースがJPEGである場合、cjxlはデフォルトでlossless_jpegを有効にするが、この状態での圧縮幅はわずかで、正直やる価値を感じない。

一方、これを無効にしてq=75で圧縮した場合、視覚的劣化はわずかで、サイズはAVIFを下回るところまで圧縮できた。 q factorの調整もAVIFよりもしやすく、速度的にもAVIFよりも大幅に速いため、写真の圧縮に関してはJPEG XLのほうが優れているように思われる。

スクリーンショットのほうはやや微妙。 q=70まで落としてもWebPよりもサイズが大きいので、写真と比べ明らかに有効性が低い。 そもそもVP8もAV1もスクリーンショットのような画像に非常に強いため、その優位性に届いていないのだろう。

これを踏まえると、時期が来れば写真はJPEG XLに変更するのが良さそうだと考えられる。

謝辞

本件にあたり、Misskey開発者のaqzさんに助言いただいた。 この場を借りて御礼申し上げたい。