Skip to main content

figif_core/hashers/
phash.rs

1//! Perceptual hash (pHash) implementation using DCT.
2
3use crate::traits::FrameHasher;
4use image::RgbaImage;
5use img_hash::{HashAlg, HasherConfig, ImageHash};
6
7/// Perceptual hash (pHash) using Discrete Cosine Transform.
8///
9/// pHash is more robust to image transformations than simpler hashes
10/// but is slower to compute. It works by analyzing the frequency
11/// components of the image.
12///
13/// # Algorithm
14///
15/// 1. Resize image to 32x32
16/// 2. Convert to grayscale
17/// 3. Apply DCT (Discrete Cosine Transform)
18/// 4. Keep only low-frequency components (top-left 8x8)
19/// 5. Compute median and create hash based on above/below median
20///
21/// # Example
22///
23/// ```ignore
24/// use figif_core::hashers::PHasher;
25/// use figif_core::traits::FrameHasher;
26///
27/// let hasher = PHasher::new();
28/// let hash = hasher.hash_frame(&image);
29/// ```
30#[derive(Debug, Clone)]
31pub struct PHasher {
32    hash_width: u32,
33    hash_height: u32,
34}
35
36impl Default for PHasher {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl PHasher {
43    /// Create a new pHash hasher with default 8x8 hash size.
44    pub fn new() -> Self {
45        Self::with_size(8, 8)
46    }
47
48    /// Create a pHash hasher with custom hash dimensions.
49    ///
50    /// The actual DCT is computed on a larger image (4x the hash size),
51    /// then only the low-frequency components are kept.
52    pub fn with_size(width: u32, height: u32) -> Self {
53        Self {
54            hash_width: width,
55            hash_height: height,
56        }
57    }
58
59    /// Get the hash width.
60    pub fn hash_width(&self) -> u32 {
61        self.hash_width
62    }
63
64    /// Get the hash height.
65    pub fn hash_height(&self) -> u32 {
66        self.hash_height
67    }
68
69    /// Get the total hash bits.
70    pub fn hash_bits(&self) -> u32 {
71        self.hash_width * self.hash_height
72    }
73
74    fn build_hasher(&self) -> img_hash::Hasher {
75        HasherConfig::new()
76            .hash_alg(HashAlg::DoubleGradient)
77            .hash_size(self.hash_width, self.hash_height)
78            .to_hasher()
79    }
80}
81
82impl FrameHasher for PHasher {
83    type Hash = ImageHash;
84
85    fn hash_frame(&self, image: &RgbaImage) -> Self::Hash {
86        let hasher = self.build_hasher();
87        // Convert to img_hash's image type via raw pixel conversion
88        let (width, height) = image.dimensions();
89        let raw = image.as_raw().clone();
90        let img_hash_image: img_hash::image::RgbaImage =
91            img_hash::image::ImageBuffer::from_raw(width, height, raw)
92                .expect("image dimensions should match");
93        let dynamic = img_hash::image::DynamicImage::ImageRgba8(img_hash_image);
94        hasher.hash_image(&dynamic)
95    }
96
97    fn distance(&self, a: &Self::Hash, b: &Self::Hash) -> u32 {
98        a.dist(b)
99    }
100
101    fn name(&self) -> &'static str {
102        "phash"
103    }
104
105    fn suggested_threshold(&self) -> u32 {
106        // pHash can tolerate slightly more difference due to its robustness
107        let base_bits = 64;
108        let actual_bits = self.hash_bits();
109        (8 * actual_bits / base_bits).max(5)
110    }
111}