画像の重複をハッシュで検出 – 類似画像グループ化ツール
画像の知覚ハッシュをすでに計算済みであれば、このエンドポイントがその平坦なリストを重複グループへと変換します。画像ごとに1件ずつ、名前とハッシュ(16進文字列または0/1のビット列)を送信し、あわせてビット単位の最大Hamming距離を指定します。その距離以内に収まるハッシュのペアはすべてリンクされ、リンクされたペアは連結グループへとマージされるため、類似画像の連鎖は重なり合うペアではなく1つのクラスタにまとまります。結果には、各グループとそのメンバー、グループ内のペアごとの距離、グループ内の最大距離が一覧表示され、さらに合計値として、何枚が重複で何枚がユニークのままかも示されます。アップロードは一切不要です。すでにお持ちのハッシュだけが、この処理に必要な入力のすべてです。
無料で実行
ハッシュのリストから重複グループへ
64ビットのpHashのような知覚ハッシュは画像の見た目をエンコードするため、圧縮やサイズ、透かしだけが異なる2枚の写真では、数ビットしか異ならないハッシュが生成されます。それらのハッシュをペアごとに比較するのは簡単です。厄介なのはその先です。画像AがBに一致し、BがCに一致するのに、AとCはぎりぎり一致しない――これは1つの重複セットでしょうか、それとも2つでしょうか。この機能は、その問いを標準的な方法で解決します。すべての画像をノードとして扱い、選択したしきい値以下のHamming距離を持つハッシュ同士の間にエッジを引き、連結成分を重複グループとして報告します。グループごとに、メンバー名、しきい値内のすべてのペアとその正確な距離、グループ内の最大距離が1件ずつ返されるため、なぜ2枚が同じグループに入ったのかを監査できます。グループ化は推移性に基づくため、1ビットずつずれていく再保存の連鎖も1つのクラスタに収まります。これこそ、実際のフォトライブラリにおける類似画像検出に期待される挙動です。
しきい値とハッシュ形式の選び方
しきい値はビット単位の最大Hamming距離で、デフォルトは5です。これは64ビットハッシュで一般的な出発点で、同一画像は0になり、見た目で区別できない再エンコードは通常5未満に収まります。完全一致またはほぼ完全一致だけが欲しい場合は0に近づけ、パイプラインで積極的にリサイズしたり端を切り落としたりする場合は慎重に引き上げてください。許容するビットが1つ増えるごとに、大規模コレクションでの誤検出の可能性が倍増するからです。ハッシュは2つの形式で受け付けます。1文字あたり4ビットでデコードされる16進文字列か、リテラルの0/1ビット列です。唯一の厳格なルールは一貫性で、1回のリクエスト内のすべてのハッシュが同じ形式を使い、同じビット長にデコードされなければなりません。さもなければ距離に意味がなくなるため、リクエストは明確なエラーで拒否されます。負のしきい値も同様に無効な入力として拒否されます。ハッシュ自体は兄弟機能のimage.phashで計算でき、このエンドポイントが期待する16文字の16進文字列をそのまま生成します。
差分比較・保存できる決定的な出力
レスポンスは、カタログの横に保存して時系列で比較できるように設計されています。グループは入力リスト内の最も早いメンバーの位置でソートされ、メンバーは入力順を保ち、内部のペアは両方の名前と正確な距離付きで列挙されるため、同じリストへの2回の実行はバイト単位で同一の出力を生成し、リグレッションはクリーンなdiffとして現れます。グループと並んで、ダッシュボードが必要とする集計値も得られます。処理した画像の総数、ハッシュのビット長、重複グループの数、いずれかのグループに入った画像の数、ユニークのまま残った画像の数です。乱数も時計もネットワークも関与しません。同じ入力には常に同じ答えが返ります。APIでも、このページの無料ブラウザランナーでも同じことで、どちらもまったく同じコードを実行しています。そのため、アセットリポジトリに新しい重複が紛れ込んだらビルドを失敗させるCIパイプラインや、重複排除レポートをメールで送る定期クリーンアップジョブでも、安心して使えます。
活用例
商品カタログの重複排除
すべての商品写真のpHashを投入し、異なるファイル名やサイズで同じ画像を使い回している出品を統合します。
ストックフォトライブラリの監査
同じセッションで撮られたほぼ同一のショットを見つけ、レビュアーが12個の再エクスポートではなく代表の1枚を残せるようにします。
アップロードパイプラインの監視
新規アップロードのハッシュが、すでに保存している画像のしきい値以内に入った場合に、拒否またはフラグを立てます。
よくある質問
料金はいくらですか?
1リクエストあたり$0.002です。このページではブラウザ上で同じコードを無料で実行することもできます。
画像そのものをアップロードするのですか?
いいえ。送信するのは、すでに計算済みの知覚ハッシュと画像ごとの名前だけです。バイナリファイルがお客様の環境から出ることはありません。
どのハッシュ形式が使えますか?
16進文字列(1文字あたり4ビット、大文字小文字は区別しません)と、リテラルの0/1ビット列です。1回のリクエスト内のすべてのハッシュは、同じ形式とビット長を共有している必要があります。
しきい値はどれくらいにすべきですか?
デフォルトは5ビットで、64ビットのpHashでは妥当な出発点です。完全一致だけが必要なら0を使い、引き上げる場合は慎重に行ってください。値を大きくすると再保存をより多く捉えますが、誤検出も増えます。
一致が重なり合う場合、グループはどう形成されますか?
連結成分によって形成されます。AがBに一致し、BがCにしきい値以内で一致すれば、AとCの距離がしきい値より遠くても、3枚すべてが1つのグループに入ります。各リンクを監査できるよう、ペアごとの正確な距離が一覧表示されます。
しきい値が負の場合はどうなりますか?
Hamming距離は負になり得ないため、リクエストは無効な入力として拒否されます。課金もされません。
開発者向け — APIアクセス
このページの機能はすべてAPIからも利用できます。自社システムに組み込みたいチーム向けのセクションです。それ以外の方は上のツールをそのままお使いください。
エンドポイント
Bearerトークンで認証し、POST1回でタスクをキューに登録します。結果はWebhookまたは署名付きリンクで受け取れます。
お使いのスタックから呼び出す
curl -X POST https://api.kit.forhosting.com/image/duplicate-detect-hash \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"images":[{"name":"hero-a.jpg","hash":"ff8f8383c3c3c3ff"},{"name":"hero-b.jpg","hash":"ff8f8383c3c3c3f7"},{"name":"hero-c.jpg","hash":"ff8f8383c3c3c3f3"},{"name":"logo.png","hash":"007c7c7c7c7c7c00"}]}'const res = await fetch("https://api.kit.forhosting.com/image/duplicate-detect-hash", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"images": [
{
"name": "hero-a.jpg",
"hash": "ff8f8383c3c3c3ff"
},
{
"name": "hero-b.jpg",
"hash": "ff8f8383c3c3c3f7"
},
{
"name": "hero-c.jpg",
"hash": "ff8f8383c3c3c3f3"
},
{
"name": "logo.png",
"hash": "007c7c7c7c7c7c00"
}
]
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/image/duplicate-detect-hash",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"images": [
{
"name": "hero-a.jpg",
"hash": "ff8f8383c3c3c3ff"
},
{
"name": "hero-b.jpg",
"hash": "ff8f8383c3c3c3f7"
},
{
"name": "hero-c.jpg",
"hash": "ff8f8383c3c3c3f3"
},
{
"name": "logo.png",
"hash": "007c7c7c7c7c7c00"
}
]
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/image/duplicate-detect-hash", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"images":[{"name":"hero-a.jpg","hash":"ff8f8383c3c3c3ff"},{"name":"hero-b.jpg","hash":"ff8f8383c3c3c3f7"},{"name":"hero-c.jpg","hash":"ff8f8383c3c3c3f3"},{"name":"logo.png","hash":"007c7c7c7c7c7c00"}]}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"images":[{"name":"hero-a.jpg","hash":"ff8f8383c3c3c3ff"},{"name":"hero-b.jpg","hash":"ff8f8383c3c3c3f7"},{"name":"hero-c.jpg","hash":"ff8f8383c3c3c3f3"},{"name":"logo.png","hash":"007c7c7c7c7c7c00"}]}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/image/duplicate-detect-hash", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)リクエスト例
{
"images": [
{
"name": "hero-a.jpg",
"hash": "ff8f8383c3c3c3ff"
},
{
"name": "hero-b.jpg",
"hash": "ff8f8383c3c3c3f7"
},
{
"name": "hero-c.jpg",
"hash": "ff8f8383c3c3c3f3"
},
{
"name": "logo.png",
"hash": "007c7c7c7c7c7c00"
}
]
}レスポンス例
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "image.duplicate_detect_hash",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}非同期APIです。task_idは即時に返ります。ポーリングは1秒あたり1リクエストまでです。
料金
単価はすべて公開しています。トークン換算や独自クレジットはありません。失敗したタスクは課金されません。
制限
max_items | 5000 |
エラー
| HTTP | コード | 意味 |
|---|---|---|
401 | unauthorized | APIキーが無効か、指定されていません。Authorizationヘッダーを確認してください。 |
402 | insufficient_balance | 残高が不足しています。チャージ後に再度お試しください。 |
404 | unknown_type | 指定されたタスクタイプは存在しません。タイプ名を確認してください。 |
429 | rate_limited | リクエストが多すぎます。しばらく待ってから再度お試しください。 |