2026年 01月 12日

golang で AVIF 対応 image.Decode

標準ないし準標準 (golang.org/x/image/webp みたいな) にはないので面倒っぽそうだなあとなんとなく思ってたけど意外にも簡単に対応できた。

CGo を許容するかで今のところ2択っぽい

  1. _ "github.com/vegidio/avif-go" CGo
  2. _ "github.com/gen2brain/avif" libavif を WASM にコンパイルし、wazero で実行

どっちもブランクインポートするだけでつかえる。ベンチ的には CGo のほうが10倍早い

こういう検証とベンチをむちゃくちゃサクっとやれるのすごいいいよなあ。Agentic Coding。脳の負荷が位置が変わっているのを感じる。

検証

vegidio/avif-go

# register_vegidio_test.go
package main

import (
	"bytes"
	"image"
	"os"
	"testing"

	_ "github.com/vegidio/avif-go"
)

func TestRegisterVegidio(t *testing.T) {
	data, err := os.ReadFile("../../static/fixtures/sample.avif")
	if err != nil {
		t.Fatal(err)
	}

	img, format, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		t.Fatalf("Vegidio registration failed: %v", err)
	}

	t.Logf("Successfully decoded %dx%d %s image using vegidio", img.Bounds().Dx(), img.Bounds().Dy(), format)
	if format != "avif" {
		t.Errorf("Expected format avif, got %s", format)
	}
}
go test -v register_vegidio_test.go   
=== RUN   TestRegisterVegidio
    register_vegidio_test.go:23: Successfully decoded 2886x2164 avif image using vegidio
--- PASS: TestRegisterVegidio (0.08s)
PASS
ok      command-line-arguments  0.313s

github.com/gen2brain/avif

# register_gen2brain_test.go
package main

import (
	"bytes"
	"image"
	"os"
	"testing"

	_ "github.com/gen2brain/avif"
)

func TestRegisterGen2brain(t *testing.T) {
	data, err := os.ReadFile("../../static/fixtures/sample.avif")
	if err != nil {
		t.Fatal(err)
	}

	img, format, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		t.Fatalf("Gen2brain registration failed: %v", err)
	}

	t.Logf("Successfully decoded %dx%d %s image using gen2brain", img.Bounds().Dx(), img.Bounds().Dy(), format)
	if format != "avif" {
		t.Errorf("Expected format avif, got %s", format)
	}
}
go test -v register_gen2brain_test.go 
=== RUN   TestRegisterGen2brain
    register_gen2brain_test.go:23: Successfully decoded 2886x2164 avif image using gen2brain
--- PASS: TestRegisterGen2brain (0.73s)
PASS
ok      command-line-arguments  1.119s

Cloudflare R2を使ってみる

容量的には 10GB までは無料で、そのあと従量課金になってもそれほど高額ではなさそう。

ただクラスB操作(参照系)がコントロールしにくいのでちょっと怖い。CDN経由のキャッシュミスだけが問題なのでファイル数が十分に少なければ恐るることはなさそう。現状ではこのサイトの全GETリクエスト(ほとんどクローラーだけど)がR2に飛んだとしても無料枠に収まる。

まず新規アップロードを R2 にするようにしてある。様子を見つつ過去分もアップロードするつもりではあるけど、その前に JPEG ファイルを全ファイル AVIF にするということをしたいのでまだやれてない。

もともと Cloudflare Registrar 契約してるので使いはじめに抵抗がない感じ。

CDNエッジでジオシティーズ的なやつを作ってみる

半年以上前に Cloudflare Workers上で Honoフレームワーク使いつつ懐しい構成のサイトを作ってみたやつ、日記に書いてなかった。

中身がシーディーエンヌのエッジのワーカーで動いてるけど見てくれがジオシティーズだったら面白いなと思って、当時は Github Copilot 使いながら作った記憶。

いわゆる6hotサイトみたいのは現状の Worker の無料枠で全然問題なかろうという気がするし、そういう意味では現代のジオシティーズなんじゃと思ったのだった。

Durable Objects まわり仕様が変化していくのでエーアイに書かせて困った思い出がある。作りっぱなし

AVIFすごすぎるな

過去のをちまちま変換してるけど、1/10 になってかつ見た目がまったく変わらないのも多い。JPEGはやめよう!

リンク切れした画像の一部を復活させた

過去画像の AVIF 化にともなって、Picasa → Google Photos → セルフホストと画像移行する過程で、うまくファイル名一致で移行できずにリンク切れになっていたファイルを、Lightroom Classic から再現像することである程度は復元した。

エントリの中にはあるがファイルシステムにないものを抽出して、

2026/01/12 17:38:39 AVIF変換を検証中...
2026/01/12 17:38:39 [欠落] エントリID:19390 (2012/04/20/1) 画像: IMG_0191-2048.jpg (AVIF:なし)
2026/01/12 17:38:39 [欠落] エントリID:19392 (2012/04/21/1) 画像: IMG_0123-2048.jpg (AVIF:なし)
2026/01/12 17:38:39 [欠落] エントリID:19393 (2012/04/22/1) 画像: IMG_0170-2048.jpg (AVIF:なし)
...
|<

この情報をGeminiに投げつけながらワンライナーでファイル名の一部を抽出してもらい、Lightroom の検索窓に投げんだ。あきらかに無関係なファイル以外をクイックコレクションに追加してすべて書き出た。

ただ、カメラのファイル名はあんまり一意ではない(買い替えのタイミングとかでリセットされている)。なので exiftool で撮影日時を取得し、↑ のログの日付と、因果律が崩れない範囲でファイル名をマッチングさせるコードを書いてもらった (なぜか Ruby で書いてくれた)

そのうえで何度か漏れたファイルを出力した。1件だけどうしても見つからないファイルがあったけどもういいかなという感じ。

過去画像を R2 に移行した

AVIF変換おわらせたあとガッと移行させた。

これで写真の表示が早くなるといいな。なんだかんだ Google Photos から移行してきてからは表示の遅さゆえに写真アップロードするモチベが落ちていたなと思う。

事前処理

もともとのファイル郡と拡張子ごとのサイズ

$ find . -type f -printf "%s %f\n" | awk '{ext=$NF; if(ext ~ /\./) {sub(/.*\./,"",ext); ext=tolower(ext)} else {ext="no_ext"}; sum[ext]+=$1} END {for(e in sum) print sum[e], e}' | sort -nr | numfmt --to=iec --field=1
5.5G jpg
298M png
60M webp
56M gif
1.5M avif
1.1M jpeg
219K ds_store

oxipng と avif への変換後

$ find . -type f -printf "%s %f\n" | awk '{ext=$NF; if(ext ~ /\./) {sub(/.*\./,"",ext); ext=tolower(ext)} else {ext="no_ext"}; sum[ext]+=$1} END {for(e in sum) print sum[e], e}' | sort -nr | numfmt --to=iec --field=1
3.3G avif
450M jpg
240M png
60M webp
56M gif

この450MBの jpg については参照されてないものっぽいのでGCすることに。

2026年 01月 11日

matplotlibのフォントキャッシュ削除

以下をコピーして pbpaste | python する

import matplotlib
import shutil
import os

# キャッシュディレクトリの場所を取得
cache_dir = matplotlib.get_cachedir()
print(f"キャッシュディレクトリ: {cache_dir}")

# キャッシュディレクトリ内の fontlist 関連ファイルを削除
for file in os.listdir(cache_dir):
    if file.startswith("fontlist") and file.endswith(".json"):
        path = os.path.join(cache_dir, file)
        print(f"削除中: {path}")
        os.remove(path)

print("削除完了。Jupyter Notebookやスクリプトを再起動してください。")

まちがえました。この日記には削除機能がないんです

管理画面のヘッダをローカルと色変えるようにしました

<!DOCTYPE html>

類似画像検索をOKLCH色空間で再実装

類似画像検索まわりをだいぶいじった。Perl の実装では Libpuzzle を適当に使ったやつだったが、独自実装に変えてみた。↑ の画像は管理画面

3次元のヒストグラム

「類似」をどうするかをこの日記においては色の傾向が似ている (形は無視する) としてみる。意図としては「雰囲気が似ている」画像を出すというのを目標にするもの。

つまりヒストグラムの比較で類似性を判断する。

人間の色認知は3次元の色空間で表現されるので、この3次元空間をそれぞれの次元で均等に区切り (バケツに比喩される)、各ピクセルをそれぞれ、その空間(バケツ)に放りこむ。

使う色空間を OKLCH に

RGB の3次元でもヒストグラムは作れるけど、距離が知覚と一定ではない問題があるので、ビットごとに知覚的な空間との間に乖離がある。ちょうどいい色空間として、人間の知覚的に均等な距離を持つ OKLCH 空間があるのでこれを使ってみることにした。

こうすることで知覚とバケツのサイズを一致させ、1ビットの価値を均等にできる。

ヒストグラムのエンコーディング (64bitのビットマスク化)

集計したヒストグラムをそのまま保存するとデータ量が大きいため、各バケツのピクセル数(頻度)を保存するのではなく、「その色が画像の中に一定以上(例えば面積の3%以上)存在するかどうか」という 0 か 1 かのフラグに変換して保存する。

これを 64個のバケツすべてで行うと、画像は 64ビットの1つの整数(シグネチャ) として表現できる。これが画像の「色の指紋」となる。

OKLCH を使うと書いたけど、L(明るさ)、C(彩度)、H(色相)を、それぞれビット位置として L:2bit (4段階) C:1bit (2段階) H:3bit (8方向) と配分した。Lab ではなく LCH を使うことで H に多くのビットを割くということができる。

これでバケツごとに1bit(有無)に情報圧縮される。バケツは「こんな感じの色」という単位なので、「こんな感じの色が含まれている」という集合の状態になる。同時にこれは画像全体のサイズによって正規化されている。

類似度の計算(Jaccard 係数)

最終的に画像同士のシグネチャ、実体としては色の集合のビットマスクを比べる際は Jaccard(ジャカード)係数 を使う。これは「共通して持っている色の数」を「両方の画像のどちらかに存在する色の総数」で割ったものであり、パレット(色の品揃え)がどれだけ重なっているかとして評価できる。

ただこれを全画像に毎回やるのは大変なので SQL で検索可能にするための工夫をする。

検索の高速化 (ngram)

似ているビットマスクを検索する手段として、ビットをいくつかに分割してngramにして保存しておくという方法が使われる。別に任意のビットマスクでよく使わる方法で特別な方法ではない。

ここでは1ビットごとにずらした12bitをそのパターンのオフセット位置を共に、52個のワードとして保存する。つまり1つの画像は64bitのシグネチャにされたあと、この64bitをスライディングウィンドウで12bitずつ切り出して保存し、これにインデックスを貼る。

そして類似画像の検索時は、このインデックスを利用して、部分マッチするものを先に足切りしてとってくる。SQL レベルで「同じ明るさ・色相の場所に、同じ色の塊を持っている画像」をピンポイントで引き抜くことができる。

空間充填曲線(Z-order)による ngram の高密度化

さらに、この検索用の12ビットの窓にも、より多くの「関連する色」を詰め込むため、Z-order (Morton order) を採用してビットのアドレスを決定する。

これにより、1次元のビット列上でも3次元的な色の近接性が保たれ、1つの ngram が「色空間上の意味のある局所的なボリューム」を指紋として表現できるようになる

具体的には図の上ように、単にLCHをそれぞれ L=2bit, C=1bit, H=3bit としてビットを構築すしたままだと、ところどころ色が離れた場所にジャンプしてしまう。これを下のようにビットインターリーブ(z-order化)することで、12bitの情報を均等にできる。

2026年 01月 10日

配信画像どうするか悩んでる

既存のをまずは最適化した。トータルで JPEG 以外も含めて 5.9GB ある。

$ jpegoptim -p --all-progressive --total *.jpg
...
Average compression (2467 files): 4.79% (292586k)
jpegoptim -p --all-progressive --total *.jpg  980.72s user 45.35s system 98% cpu 17:24.23 total

約 292MB削減。ロスレス最適化だけで結構減ったなあ。

全体を AVIF 化してしまうのをやったほうがいい気もする。

まず新規アップロードについて JPEG でアップロードしたら自動で AVIF に変換かけることにした。ただ、これが非常に重かったのでいろいろ妥協した以下のようにした。

avifenc --jobs 3 --speed 8 --yuv 420 -q 60

Lightroom からは AVIF で出すので、写真を JPEG でアップロードするのはあんまりこだわらないときに限られるのでいいだろう……

あと PNG に関しても oxipng か optipng が入っていたら自動で通すようにした。スクショとか上げることが多いのでこれは効くはず

全JPEGのAVIF化で問題になるのは、あとはOGP画像。OGP画像は今まではエントリの最初の img の src をそのままブチこむということをしてたけど、これができなくなる。ウーン。まあ諦めて普通のOGP画像を出せばいいか。大して共有されるわけではないから

Ubuntu 20.04 に avifenc を入れる

README.md に書いてある通りだけど、ext/ 以下で libaom をビルドしてそれを static link するのが良いようだ

git clone -b v1.3.0 --depth 1 https://github.com/AOMediaCodec/libavif.git
cd libavif
cd ext
sh aom.cmd
cd ..
cmake -DAVIF_BUILD_APPS=ON -DBUILD_SHARED_LIBS=OFF -DAVIF_CODEC_AOM=LOCAL -DAVIF_LIBYUV=LOCAL . 

v1.3.0 より新しいと要求する cmake があたらしすぎてダメだった

Ubuntu 20 から 24 に do-release-upgrade

LLM にリリースノートと ps aux の結果とかを食わせながら注意点を適当にあらいだしてやった。こういうときペアでやってる感じがあってだいぶ気が楽だ。

そこそこすんなりいったのでよかった。

h2o

openssl のアップグレードがあるので h2o を一時的に static link に。前回アップグレード時もビルドしなおしになってハマった記憶

h2o 用の openssl を配置しとく

 wget https://www.openssl.org/source/openssl-3.0.12.tar.gz
 tar xzvf openssl-3.0.12.tar.gz 
 cd openssl*
./config --prefix=/opt/h2o-ssl no-shared
make
sudo make install

ビルドしなおし

$ git clean -fdx
$ export PKG_CONFIG_PATH=/opt/h2o-ssl/lib64/pkgconfig  
$ cmake -DOPENSSL_ROOT_DIR=/opt/h2o-ssl -DOPENSSL_USE_STATIC_LIBS=TRUE  -DWITH_FUSION=ON -DCMAKE_C_FLAGS="-march=native" .
$ make
$ make install

$ ./h2o -v
h2o version 2.3.0-DEV@afe696c72
OpenSSL: OpenSSL 3.0.12 24 Oct 2023
mruby: YES
capabilities: YES
fusion: YES
ssl-zerocopy: YES
ktls: YES
key-exchanges: secp521r1, secp384r1, x25519, secp256r1

$ ldd ./h2o
        linux-vdso.so.1 (0x00007ffef3ffb000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f411d0e2000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f411d0dc000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f411cf8d000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f411cf71000)
        libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 (0x00007f411cf68000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f411cd76000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f411d951000)

バックアップ

念のため

sudo tar czvf /data/vps_config_backup_20260110_v2.tar.gz \
  /home \         
  /etc \
  /usr/local \
  /var/lib \
  /var/spool/cron \
  /root \
  /opt \
  /srv \
  /var/www \
  --exclude=/var/lib/docker \
  --exclude="/home/*/tmp" \
  --exclude="/home/*/swp"

sudo do-release-upgrade

20 → 22 → 24 とあげていった。普通に起動してくれてよかった

rabbitmq

mqtt server としてしか使ってない割に do-release-upgrade のたびに障壁になってたので、使うのをやめた。

mochi-mqtt/server + すこしの実装で、自分の要求には完全な代替を Gemini が書いてくれたので、それをsystemdでデーモン化した。

mackerel-agent

インストールスクリプト流しなおした

certbot

動かなくなっていたので snap 版をつかうようにした。

 sudo apt install snapd
 sudo snap install core; sudo snap refresh core
 sudo snap install --classic certbot
 sudo ln -s /snap/bin/certbot /usr/bin/certbot

systemd と Unix domain socket

2021年にh2oをsystemd管理にしてから、UNIXドメインソケットでの接続 (unix:/tmp/backend とかにしてた) がうまくいかず、ずっと TCP を無駄に通してた。

これがいまさらながら解決した。つまり /tmp は systemd が PrivateTmp で隔離しているので共有できないというだけだった。

なので以下のように RuntimeDirectory の設定を入れる。

RuntimeDirectory=hanrangon
RuntimeDirectoryMode=0755

これで /run/hanrangon が systemd によって作られるので、ここの下に /run/hanrangon/socket として UNIXドメインソケットを作るといいらしい。

フロント(h2o)からは http://[unix:/run/hanrangon/socket] を指定する。