face-intel / cores /vision /hashing.py
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
3.7 kB
"""Hashing — cryptographic (SHA-256) + perceptual (pHash, dHash, aHash, wHash).
Consolidates every hashing need into one module so cache keys, duplicate
detection, and integrity checks all use identical implementations.
"""
from __future__ import annotations
import hashlib
import cv2
import numpy as np
# --------------------------------------------------------------------------- #
# Cryptographic
# --------------------------------------------------------------------------- #
def sha256_bytes(data: bytes) -> str:
"""SHA-256 hex digest of raw bytes."""
return hashlib.sha256(data).hexdigest()
def sha256_image(img: np.ndarray, quality: int = 90) -> str:
"""SHA-256 of the JPEG-encoded image — stable cache key."""
ok, buffer = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, quality])
if not ok:
raise ValueError("Could not encode image for hashing.")
return sha256_bytes(buffer.tobytes())
# --------------------------------------------------------------------------- #
# Perceptual
# --------------------------------------------------------------------------- #
def phash(img: np.ndarray, hash_size: int = 8) -> str:
"""pHash: DCT-based perceptual hash. Returns 64-bit string."""
gray = _to_gray(img)
resized = cv2.resize(gray, (hash_size * 4, hash_size * 4), interpolation=cv2.INTER_AREA)
dct = cv2.dct(np.float32(resized))
dct_low = dct[:hash_size, :hash_size]
median = np.median(dct_low)
bits = (dct_low > median).flatten()
return _bits_to_hex(bits)
def dhash(img: np.ndarray, hash_size: int = 8) -> str:
"""dHash: difference-based perceptual hash."""
gray = _to_gray(img)
resized = cv2.resize(gray, (hash_size + 1, hash_size), interpolation=cv2.INTER_AREA)
diff = resized[:, 1:] > resized[:, :-1]
return _bits_to_hex(diff.flatten())
def ahash(img: np.ndarray, hash_size: int = 8) -> str:
"""aHash: average hash."""
gray = _to_gray(img)
resized = cv2.resize(gray, (hash_size, hash_size), interpolation=cv2.INTER_AREA)
avg = resized.mean()
bits = (resized > avg).flatten()
return _bits_to_hex(bits)
def whash(img: np.ndarray, hash_size: int = 8) -> str:
"""wHash: wavelet hash (Haar wavelet)."""
try:
import pywt
except ImportError:
# Fall back to pHash if PyWavelets not available
return phash(img, hash_size)
gray = _to_gray(img)
resized = cv2.resize(gray, (hash_size * 2, hash_size * 2), interpolation=cv2.INTER_AREA)
coeffs = pywt.dwt2(resized, "haar")
ll, _ = coeffs
median = np.median(ll)
bits = (ll > median).flatten()
return _bits_to_hex(bits)
def hamming_distance(a: str, b: str) -> int:
"""Hamming distance between two hex hash strings."""
if len(a) != len(b):
return max(len(a), len(b))
try:
ai = int(a, 16)
bi = int(b, 16)
except ValueError:
return sum(c1 != c2 for c1, c2 in zip(a, b))
return bin(ai ^ bi).count("1")
# --------------------------------------------------------------------------- #
# Internal
# --------------------------------------------------------------------------- #
def _to_gray(img: np.ndarray) -> np.ndarray:
if img.ndim == 2:
return img
if img.shape[2] == 4:
return cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
def _bits_to_hex(bits: np.ndarray) -> str:
"""Convert a boolean array to a hex string."""
bits_str = "".join("1" if b else "0" for b in bits)
# Pad to multiple of 4
while len(bits_str) % 4 != 0:
bits_str += "0"
return "".join(hex(int(bits_str[i:i+4], 2))[2:] for i in range(0, len(bits_str), 4))