feat(phash): compute_phash + find_similar util (hash_size=8, fits existing column)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
"""Perceptual-hash dedup helpers (ported from ImageRepo).
|
||||
|
||||
hash_size=8 -> 64-bit hash -> 16-hex-char string, which fits the existing
|
||||
ImageRecord.phash String(32) column (no image_record migration). IR uses
|
||||
hash_size=16; the deliberate FC deviation keeps the schema unchanged. The
|
||||
Hamming threshold is the operator-exposed dial (ImportSettings).
|
||||
"""
|
||||
|
||||
import imagehash
|
||||
|
||||
HASH_SIZE = 8
|
||||
|
||||
|
||||
def compute_phash(pil_image) -> str | None:
|
||||
"""Perceptual hash of an opened PIL image, as a hex string. None on any
|
||||
failure (videos/unreadable/non-image)."""
|
||||
try:
|
||||
return str(imagehash.phash(pil_image, hash_size=HASH_SIZE))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def find_similar(
|
||||
phash_hex: str,
|
||||
width: int,
|
||||
height: int,
|
||||
candidates: list[tuple[str, int, int, int]],
|
||||
threshold: int,
|
||||
) -> tuple[str, int | None]:
|
||||
"""candidates: (phash_hex, width, height, image_id). Returns one of
|
||||
("none", None) / ("larger_exists", id) / ("smaller_exists", id).
|
||||
First qualifying candidate wins (IR loop order)."""
|
||||
new_h = imagehash.hex_to_hash(phash_hex)
|
||||
for cand_hex, cw, ch, cid in candidates:
|
||||
try:
|
||||
dist = new_h - imagehash.hex_to_hash(cand_hex)
|
||||
except Exception:
|
||||
continue
|
||||
if dist <= threshold:
|
||||
if cw >= width and ch >= height:
|
||||
return ("larger_exists", cid)
|
||||
if width > cw or height > ch:
|
||||
return ("smaller_exists", cid)
|
||||
return ("none", None)
|
||||
Reference in New Issue
Block a user