Files
FabledCurator/backend/app/utils/phash.py
T
2026-05-17 22:02:42 -04:00

45 lines
1.4 KiB
Python

"""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)