Speed: dhash-only rows (0.21s→6ms/file); gate near-dup+phash behind flag; fix hx UnboundLocalError; loop in single task (engine deadlock fix); photo_watch uses ingest flow

This commit is contained in:
2026-08-08 15:30:34 +10:00
parent 32978030c3
commit 5fb9358b5f
3 changed files with 87 additions and 35 deletions

View File

@@ -82,14 +82,21 @@ def sha256_file(path: Path) -> str:
return h.hexdigest()
def hash_image(path: Path, hash_size: int = 8):
"""Perceptual hashes for one image file."""
def hash_image(path: Path, hash_size: int = 8, include_phash: bool = False):
"""Perceptual hashes for one image file.
dhash is cheap (~6ms); phash is expensive (~210ms) so only computed when
include_phash=True (near-dup checks). The stored row uses dhash as the
dedup fingerprint — dhash is a valid perceptual hash for this purpose.
"""
with Image.open(path) as im:
im = im.convert("RGB")
return {
"phash": str(phash(im, hash_size=hash_size)),
"dhash": str(dhash(im, hash_size=hash_size)),
}
hx = {"dhash": str(dhash(im, hash_size=hash_size))}
if include_phash:
hx["phash"] = str(phash(im, hash_size=hash_size))
else:
hx["phash"] = ""
return hx
def register_image(conn, path: Path, source: str, exif_date: str | None = None):
@@ -122,8 +129,11 @@ def check_near_dup(conn, path: Path, hamming_threshold: int = 10):
rows = conn.execute("SELECT phash, dhash, path, source FROM image_hashes").fetchall()
best, best_dist = None, None
for ph, dh, p, src in rows:
phd = hamming(ph, hx["phash"])
dhd = hamming(dh, hx["dhash"])
dhd = hamming(dh, hx.get("dhash", ""))
if ph and hx.get("phash"):
phd = hamming(ph, hx["phash"])
else:
phd = dhd # no phash available — use dhash distance
dist = min(phd, dhd)
if best_dist is None or dist < best_dist:
best, best_dist = (p, src), dist
@@ -168,8 +178,11 @@ def _near_dup_lookup(conn, hx: dict, hamming_threshold: int = 10):
rows = conn.execute("SELECT phash, dhash, path, source FROM image_hashes").fetchall()
best, best_dist = None, None
for ph, dh, p, src in rows:
phd = hamming(ph, hx["phash"])
dhd = hamming(dh, hx["dhash"])
dhd = hamming(dh, hx.get("dhash", ""))
if ph and hx.get("phash"):
phd = hamming(ph, hx["phash"])
else:
phd = dhd # no phash available — use dhash distance
dist = min(phd, dhd)
if best_dist is None or dist < best_dist:
best, best_dist = (p, src), dist