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