File size: 3,703 Bytes
892fa81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""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))