Jimp の hash はすぐに衝突するので注意する


Node.js の画像処理によく使われる Jimp だが、思いがけない落とし穴(バグではない)があったのでメモ。

全く異なる2つの画像のハッシュ値が同じになってしまう

  • 次の2つの画像を Jimp で hash すると、同じハッシュ値 80000000000 (base64) になる

  • ハッシュ値が衝突してしまうことは「起こりうる事象」なのだが、 Jimp のハッシュはかなり頻繁に衝突するので、なるべくユニークな値が欲しい場合は諦めて別の方法でハッシュした方がいい

  • バグなのかとも思ったが、3年前に Issue が立ったまま放置されている

    • 💭 放置されていることで誰かを責める意図はないが、ドキュメントに分かりやすく注意書きをした方がいいかも知れないな、とは思っている
    • 💭 というか、base64 で 80000000000 って2進数だと 1000 0000 .... 0000 なので、やっぱり実装に問題があるのかも?

Jimp が採用しているハッシュアルゴリズム - pHash

  • Jimp は pHash というアルゴリズムを採用していると README に書いてある
  • pHash とは、ざっくりいうと、視覚的な差とハッシュの差が近くなるように実装されたハッシュらしい
    • つまり MD5 や SHA のように「ほんの少しの違いでハッシュ値が大きく変わる」という特徴を持っていない
    • 💭 Jimp の pHash 実装を読んでみると 、「高周波成分をなくすために 32x32 に縮小します」「画像をグレースケールに変換します」とか書かれていて、目玉が飛び出そうになった
    • 💭 間違っても画像の一致判定、特に暗号学的な用途には使えない

Jimp.distance() の落とし穴

  • Jimp には2つの画像の差を求める関数として Jimp.distanceJimp.diff の2つを用意している
  • Jimp.distance() は pHash で互いのハッシュ値を求めたあと、単に2つのハミング距離を返す関数である
  • 前述の説明から分かるとおり、 Jimp.distance(a, b) === 0 は a と b が一致していることを意味しない
    • a と b が一致していれば 0 になるが、そうでなくても 0 になるケースがある(前述の例でもやはり 0 になる)
Jimp.distance(image1, image2); // returns a number 0-1, where 0 means the two images are perceived to be identical`

💭 ドキュメントには上のように書かれているので、勘違いする人がいるかも知れない。 単に僕の英語力の問題かも知れないが、 perceived to be identical は明らかに間違っているのでは……(自信がない)

参考までに pHash.org は次のように表現している

perceptual hashes are "close" to one another if the features are similar.
http://www.phash.org/

💭 ちなみに、 Jimp.diffPixelMatch というライブラリでピクセル単位で画像を比較するので、ドキュメントには両者を組み合わせることで "the same image" (意味は下の文を読んでください) を発見出来ると書かれている

Using a mix of hamming distance and pixel diffing to compare images, the following code has a 99% success rate of detecting the same image from a random sample (with 1% false positives). The test this figure is drawn from attempts to match each image from a sample of 120 PNGs against 120 corresponding JPEGs saved at a quality setting of 60.
https://github.com/oliver-moran/jimp/tree/master/packages/jimp#comparing-images

まとめ

  • 単に画像のハッシュ値を得るために Jimp の hash を使うべきではない
    • Node.js なら標準の crypto.createHash ないしそのラッパーを使うといい
  • Jimp.distance(a, b) === 0 は a と b が一致していることを意味しない
    • 単に画像の一致を高速に判定したいなら Jimp.diff (PixelMatch) を使う