Files
photochk/docs/calibration.md
T
yfoxjpandClaude Opus 5 c7f68b6cca Photochk 1.0.0 — 類似写真を探して整理する macOS アプリ
フォルダ内の写真から、中身が同じものと見た目が似ているものを検出して
グループにまとめ、残す1枚を選んでほかをゴミ箱へ送れるようにする。

検出は安い判定から順に4段:
- ファイルサイズが同じものだけ SHA-256 でバイト一致を確定
- dHash(知覚ハッシュ)のハミング距離で候補ペアを抽出
- 候補に出てくる画像だけ Vision の feature print で精査
- Union-Find で連結成分をグループ化

判定のしきい値は実写画像で距離を実測して決めた(docs/calibration.md)。
tools/release-update.sh で Gitea のリリースを更新する。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 08:02:14 +09:00

3.5 KiB
Raw Permalink Blame History

しきい値の実測記録

Sensitivity のしきい値は当てずっぽうではなく、実写画像で距離を測って決めている。 判定を変えたくなったらこの手順をなぞって測り直すこと。

測定に使った素材

macOS 内蔵の動画壁紙(実写のタイムラプス)からフレームを切り出したものを元画像とした。

ファイル 内容
shotA.jpg 元画像(1600×900, JPEG)
A_copy.jpg cp によるバイト単位の完全コピー
A_half.jpg 50% に縮小
A_quarter.jpg 25% に縮小
A_lowq.jpg 同じ寸法で JPEG 品質 25 に再圧縮
A_crop.jpg 中央 1400×900 にトリミング
A_rot90.jpg ピクセルごと 90 度回転
shotA_burst.jpg 0.5 秒後のフレーム(連写相当)
shotB.jpg / shotC.jpg 30 秒後 / 90 秒後のフレーム(同一シーンの別カット)
sonoma.jpg まったく別の写真

測定結果(Vision の feature print 距離 / dHash のハミング距離)

shotA.jpg を基準にした値。

相手 feature print ハミング 判定したい?
完全コピー・再エンコード 0.000 0 ○(SHA-256 で先に確定)
50% 縮小 0.049 5 ○
低画質 再圧縮 0.116 1 ○
25% 縮小 0.162 4 ○
連写(0.5 秒差) 0.008 0 ○
同一シーン 30 秒差 0.017 6 ○
同一シーン 90 秒差 0.039 14 ○
トリミング 0.417 11 「ゆるい」でのみ
90 度回転 0.802 28 ✗(後述)
無関係な写真 1.378 24 ✗

決めたしきい値

段階 feature print ハミング 何が拾えるか
厳しい 0.20 8 コピー・縮小(25% まで)・再圧縮
標準 0.30 12 上記+連写・同一シーンの近いカット
ゆるい 0.55 20 上記+トリミングされた版

「ゆるい」の 0.55 でも無関係な写真の 1.38 とは大きく離れているので、誤検出の余地は小さい。 当初案の「厳しい = 0.15」では 25% 縮小(0.162)を取りこぼしたため 0.20 に上げた。

回転の扱い(意図的な仕様)

dHash も Vision も EXIF の向きを反映してから比較している。つまり 「画面に表示したときの見た目」どうしを比べている。

  • 回転がピクセルに焼き込まれた版と、EXIF の向きタグで回している版 → 距離 0.002 で検出できる (LINE や AirDrop を経由して向きが焼き込まれた、という現実的なケース)
  • 実際に向きが違う写真どうし(元画像 vs 90 度回転) → 距離 0.80 で検出しない

後者を拾うには4方向ぶんの特徴量を計算する必要があり、コストが4倍になる割に 写真整理の用途では出番が少ないため、対応していない。

測り直す手順

ffmpeg -ss 0 -i "/System/Library/Desktop Pictures/.wallpapers/Tahoe Day/Tahoe Day.mov" -frames:v 1 -vf scale=1600:-1 shotA.jpg
sips -Z 800 shotA.jpg --out A_half.jpg
sips -s format jpeg -s formatOptions 25 shotA.jpg --out A_lowq.jpg
sips -c 900 1400 shotA.jpg --out A_crop.jpg

これらを1つのフォルダに置き、GenerateImageFeaturePrintRequest(cropAndScaleAction = .scaleToFit)で 総当たりの距離行列を出す。アプリ本体の FeaturePrinter.compute(for:) と同じ設定であることを確認すること。