gazou-compressor.jp

知覚ハッシュ(pHash/dHash/aHash)でニア重複画像を検出する実務ガイド

画像のニア重複 (near duplicate) を放置するとストレージ増大・キャッシュ効率低下・検索結果の冗長化を招きます。perceptual hash は視覚特徴を 64bit などの小さなビット列へ圧縮し、Hamming 距離で類似度を高速に近似する手法です。本稿は pHash/dHash/aHash の違いと、閾値・インデックス・運用ワークフローを「壊れにくい実務手順」に整理します。

先に結論 (TL;DR)
  • pHash + dHash の併用 AND/OR ルールで誤検出を抑制。
  • 閾値は代表データで距離分布→F1最大付近を採用 (pHash≈8–12)。
  • インデックスは8bitチャンク倒立方式で数百万件でも高速。
  • 代表画像を決めグループID管理(キャッシュ統合/権利照合/重複抑制)。

1. なぜ perceptual hash か

ピクセル完全一致 (MD5/SHA1) ではリサイズや軽微な色調整で別物扱いになります。pHash 等は“知覚”に影響の小さい変換を吸収して距離を保ちます。

2. pHash / dHash / aHash の比較

実務では pHash と dHash を併用し、双方の閾値を満たす/片方のみ等のルールで誤検出と漏れを調整。

3. 実装(ハッシュ計算)

// perceptual-hash.ts — pHash & dHash 実装例(簡略)
import sharp from 'sharp';

export async function phash(buf: Buffer): Promise<bigint> {
  // 1) 32x32 & グレースケール
  const raw = await sharp(buf).resize(32, 32, { fit: 'cover' }).greyscale().raw().toBuffer();
  // 2) DCT (ここでは簡略: 行ごとに離散コサイン近似)
  const N = 32;
  const dct: number[] = [];
  for (let u = 0; u < 8; u++) {
    for (let v = 0; v < 8; v++) {
      let sum = 0;
      for (let x = 0; x < N; x++) {
        for (let y = 0; y < N; y++) {
          const px = raw[x * N + y];
          sum += px * Math.cos(((2 * x + 1) * u * Math.PI) / (2 * N)) * Math.cos(((2 * y + 1) * v * Math.PI) / (2 * N));
        }
      }
      dct.push(sum);
    }
  }
  const coeffs = dct.slice(1, 64); // DC除外 63要素
  const median = coeffs.slice().sort((a, b) => a - b)[Math.floor(coeffs.length / 2)];
  let bits = 0n;
  coeffs.forEach((c, i) => { if (c > median) bits |= 1n << BigInt(i); });
  return bits;
}

export async function dhash(buf: Buffer): Promise<bigint> {
  const w = 9, h = 8;
  const raw = await sharp(buf).resize(w, h, { fit: 'cover' }).greyscale().raw().toBuffer();
  let bits = 0n, i = 0;
  for (let y = 0; y < h; y++) {
    for (let x = 0; x < w - 1; x++) {
      const p = raw[y * w + x];
      const q = raw[y * w + x + 1];
      if (p > q) bits |= 1n << BigInt(i);
      i++;
    }
  }
  return bits;
}

export function hamming(a: bigint, b: bigint): number {
  let x = a ^ b;
  let d = 0;
  while (x) { x &= x - 1n; d++; }
  return d;
}

4. インデックス戦略

64bit を 8bit * 8 のチャンクに分割し (位置情報もキーに含む) 倒立インデックスを形成、1〜2チャンク一致で候補集合を生成後 Hamming 距離を正確計算。

// inverted-index.ts — 64bitを8bitチャンク化し高速候補生成
type Entry = { id: number; hash: bigint };
const buckets: Map<number, number[]> = new Map(); // key=8bit, value=Entry.id list
const data: Entry[] = [];

export function add(id: number, hash: bigint) {
  data.push({ id, hash });
  for (let i = 0; i < 8; i++) {
    const part = Number((hash >> BigInt(i * 8)) & 0xffn);
    const key = (i << 8) | part; // チャンク位置も含め衝突低減
    if (!buckets.has(key)) buckets.set(key, []);
    buckets.get(key)!.push(id);
  }
}

export function candidates(hash: bigint): number[] {
  const seen = new Set<number>();
  for (let i = 0; i < 8; i++) {
    const part = Number((hash >> BigInt(i * 8)) & 0xffn);
    const key = (i << 8) | part;
    for (const id of buckets.get(key) ?? []) seen.add(id);
  }
  return [...seen];
}

5. 閾値チューニング

ラベル付きサンプルを収集し距離分布を可視化。F1 / ROC AUC を指標に最適点を探す。

# 距離分布を可視化(擬似コード)
# pairs.csv: id1,id2,phash_distance,dhash_distance,label(dup|diff)
import pandas as pd
import seaborn as sns

if __name__ == '__main__':
  df = pd.read_csv('pairs.csv')
  sns.histplot(df[df.label=='dup'].phash_distance, color='red', alpha=.5)
  sns.histplot(df[df.label=='diff'].phash_distance, color='blue', alpha=.5)
  # 距離閾値候補を探索してF1計算
  for t in range(4,20):
    tp = ((df.label=='dup') & (df.phash_distance<=t)).sum()
    fp = ((df.label=='diff') & (df.phash_distance<=t)).sum()
    fn = ((df.label=='dup') & (df.phash_distance>t)).sum()
    precision = tp / max(1,(tp+fp))
    recall = tp / max(1,(tp+fn))
    f1 = 2*precision*recall/max(1e-9,(precision+recall))
    print(t, f1)

6. ワークフロー設計

  1. バッチ初期読み込み → ハッシュ計算 → グルーピング。
  2. 代表画像選定 (最小ファイル / 最高品質 / メタ完備)。
  3. 新規投入は差分ハッシュ → インデックス候補 → 距離判定 → グループID採番。
  4. 閾値近傍 (境界ケース) は人工審査キューへ。

7. 公開前チェック

8. まとめ

perceptual hash は軽量かつ実装容易。pHash+dHash 併用とチャンク倒立インデックスで数百万規模までスケールし、ニア重複の整理・キャッシュ統合・権利チェックの高速化に寄与します。

公開:2025-09-06 / 監修:gazou-compressor.jp 編集部

gazou-compressor.jp 編集部

画像圧縮・変換・背景除去などの実践テクニックと、Webで“速く・軽く・崩さない”ためのノウハウを発信しています。

関連記事

トピック/更新日の近いコンテンツ