Skip to main content

figif_core/hashers/
dhash.rs

1//! Difference hash (dHash) implementation.
2
3use crate::traits::FrameHasher;
4use image::RgbaImage;
5use img_hash::{HashAlg, HasherConfig, ImageHash};
6
7/// Difference hash (dHash) for fast duplicate detection.
8///
9/// dHash computes a gradient-based hash by comparing adjacent pixels.
10/// It's very fast and works well for detecting near-duplicate frames.
11///
12/// # Algorithm
13///
14/// 1. Resize image to (hash_width + 1, hash_height)
15/// 2. Convert to grayscale
16/// 3. Compare each pixel to its right neighbor
17/// 4. Set bit to 1 if left > right, else 0
18///
19/// # Example
20///
21/// ```ignore
22/// use figif_core::hashers::DHasher;
23/// use figif_core::traits::FrameHasher;
24///
25/// let hasher = DHasher::new();
26/// let hash = hasher.hash_frame(&image);
27/// ```
28#[derive(Debug, Clone)]
29pub struct DHasher {
30    hash_width: u32,
31    hash_height: u32,
32}
33
34impl Default for DHasher {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl DHasher {
41    /// Create a new dHash hasher with default 8x8 hash size.
42    pub fn new() -> Self {
43        Self::with_size(8, 8)
44    }
45
46    /// Create a dHash hasher with custom hash dimensions.
47    ///
48    /// Larger sizes provide more precision but slower comparison.
49    /// Common sizes: 8x8 (64-bit), 16x16 (256-bit).
50    pub fn with_size(width: u32, height: u32) -> Self {
51        Self {
52            hash_width: width,
53            hash_height: height,
54        }
55    }
56
57    /// Get the hash width.
58    pub fn hash_width(&self) -> u32 {
59        self.hash_width
60    }
61
62    /// Get the hash height.
63    pub fn hash_height(&self) -> u32 {
64        self.hash_height
65    }
66
67    /// Get the total hash bits.
68    pub fn hash_bits(&self) -> u32 {
69        self.hash_width * self.hash_height
70    }
71
72    fn build_hasher(&self) -> img_hash::Hasher {
73        HasherConfig::new()
74            .hash_alg(HashAlg::Gradient)
75            .hash_size(self.hash_width, self.hash_height)
76            .to_hasher()
77    }
78}
79
80impl FrameHasher for DHasher {
81    type Hash = ImageHash;
82
83    fn hash_frame(&self, image: &RgbaImage) -> Self::Hash {
84        let hasher = self.build_hasher();
85        // Convert to img_hash's image type via raw pixel conversion
86        let (width, height) = image.dimensions();
87        let raw = image.as_raw().clone();
88        let img_hash_image: img_hash::image::RgbaImage =
89            img_hash::image::ImageBuffer::from_raw(width, height, raw)
90                .expect("image dimensions should match");
91        let dynamic = img_hash::image::DynamicImage::ImageRgba8(img_hash_image);
92        hasher.hash_image(&dynamic)
93    }
94
95    fn distance(&self, a: &Self::Hash, b: &Self::Hash) -> u32 {
96        a.dist(b)
97    }
98
99    fn name(&self) -> &'static str {
100        "dhash"
101    }
102
103    fn suggested_threshold(&self) -> u32 {
104        // For 8x8 (64-bit) hash, 5 is a good threshold
105        // Scale proportionally for other sizes
106        let base_bits = 64;
107        let actual_bits = self.hash_bits();
108        (5 * actual_bits / base_bits).max(3)
109    }
110}