visual_hash/
lib.rs

1//! A crate that provides several perceptual hashing algorithms for images.
2//! Supports images opened with the [image] crate from Piston.
3//!
4//! ```rust,no_run
5//! # use img_hash::{HasherConfig, HashAlg};
6//! let image1 = image::open("image1.png").unwrap();
7//! let image2 = image::open("image2.png").unwrap();
8//!     
9//! let hasher = HasherConfig::new().to_hasher();
10//!
11//! let hash1 = hasher.hash_image(&image1);
12//! let hash2 = hasher.hash_image(&image2);
13//!     
14//! println!("Image1 hash: {}", hash1.to_base64());
15//! println!("Image2 hash: {}", hash2.to_base64());
16//!     
17//! println!("Hamming Distance: {}", hash1.dist(&hash2));
18//! ```
19//! [image]: https://github.com/PistonDevelopers/image
20#![deny(missing_docs)]
21
22use std::{borrow::Cow, fmt, marker::PhantomData};
23
24mod alg;
25mod dct;
26mod traits;
27
28use dct::DctCtxt;
29use image::imageops;
30use image::GrayImage;
31use serde::{Deserialize, Serialize};
32
33pub use image::imageops::FilterType;
34
35pub use alg::HashAlg;
36
37pub(crate) use traits::BitSet;
38
39pub use traits::{DiffImage, HashBytes, Image};
40
41/// **Start here**. Configuration builder for [`Hasher`](::Hasher).
42///
43/// Playing with the various options on this struct allows you to tune the performance of image
44/// hashing to your needs.
45///
46/// Sane, reasonably fast defaults are provided by the [`::new()`](#method.new) constructor. If
47/// you just want to start hashing images and don't care about the details, it's as simple as:
48///
49/// ```rust
50/// use img_hash::HasherConfig;
51///
52/// let hasher = HasherConfig::new().to_hasher();
53/// // hasher.hash_image(image);
54/// ```
55///
56/// # Configuration Options
57/// The hash API is highly configurable to tune both performance characteristics and hash
58/// resilience.
59///
60/// ### Hash Size
61/// Setter: [`.hash_size()`](#method.hash_size)
62///
63/// Dimensions of the final hash, as width x height, in bits. A hash size of `8, 8` produces an
64/// 8 x 8 bit (8 byte) hash. Larger hash sizes take more time to compute as well as more memory,
65/// but aren't necessarily better for comparing images. The best hash size depends on both
66/// the [hash algorithm](#hash-algorithm) and the input dataset. If your images are mostly
67/// wide aspect ratio (landscape) then a larger width and a smaller height hash size may be
68/// preferable. Optimal values can really only be discovered empirically though.
69///
70/// (As the author experiments, suggested values will be added here for various algorithms.)
71///
72/// ### Hash Algorithm
73/// Setter: [`.hash_alg()`](#method.hash_alg)
74/// Definition: [`HashAlg`](enum.HashAlg.html)
75///
76/// Multiple methods of calculating image hashes are provided in this crate under the `HashAlg`
77/// enum. Each algorithm is different but they all produce the same size hashes as governed by
78/// `hash_size`.
79///
80/// ### Hash Bytes Container / `B` Type Param
81/// Use [`with_bytes_type::<B>()`](#method.with_bytes_type) instead of `new()` to customize.
82///
83/// This hash API allows you to specify the bytes container type for generated hashes. The default
84/// allows for any arbitrary hash size (see above) but requires heap-allocation. Instead, you
85/// can select an array type which allows hashes to be allocated inline, but requires consideration
86/// of the possible sizes of hash you want to generate so you don't waste memory.
87///
88/// Another advantage of using a constant-sized hash type is that the compiler may be able to
89/// produce more optimal code for generating and comparing hashes.
90///
91/// ```rust
92/// # use img_hash::*;
93///
94/// // Use default container type, good for any hash size
95/// let config = HasherConfig::new();
96///
97/// /// Inline hash container that exactly fits the default hash size
98/// let config = HasherConfig::with_bytes_type::<[u8; 8]>();
99/// ```
100///
101#[derive(Serialize, Deserialize)]
102pub struct HasherConfig<B = Box<[u8]>> {
103    width: u32,
104    height: u32,
105    gauss_sigmas: Option<[f32; 2]>,
106    #[serde(with = "SerdeFilterType")]
107    resize_filter: FilterType,
108    dct: bool,
109    hash_alg: HashAlg,
110    _bytes_type: PhantomData<B>,
111}
112
113impl HasherConfig<Box<[u8]>> {
114    /// Construct a new hasher config with sane, reasonably fast defaults.
115    ///
116    /// A default hash container type is provided as a default type parameter which is guaranteed
117    /// to fit any hash size.
118    pub fn new() -> Self {
119        Self::with_bytes_type()
120    }
121
122    /// Construct a new config with the selected [`HashBytes`](trait.HashBytes.html) impl.
123    ///
124    /// You may opt for an array type which allows inline allocation of hash data.
125    ///
126    /// ### Note
127    /// The default hash size requires 64 bits / 8 bytes of storage. You can change this
128    /// with [`.hash_size()`](#method.hash_size).
129    pub fn with_bytes_type<B_: HashBytes>() -> HasherConfig<B_> {
130        HasherConfig {
131            width: 8,
132            height: 8,
133            gauss_sigmas: None,
134            resize_filter: FilterType::Lanczos3,
135            dct: false,
136            hash_alg: HashAlg::Gradient,
137            _bytes_type: PhantomData,
138        }
139    }
140}
141
142impl<B: HashBytes> HasherConfig<B> {
143    /// Set a new hash width and height; these can be the same.
144    ///
145    /// The number of bits in the resulting hash will be `width * height`. If you are using
146    /// a fixed-size `HashBytes` type then you must ensure it can hold at least this many bits.
147    /// You can check this with [`HashBytes::max_bits()`](#method.max_bits).
148    ///
149    /// ### Rounding Behavior
150    /// Certain hash algorithms need to round this value to function properly:
151    ///
152    /// * [`DoubleGradient`](enum.HashAlg.html#variant.DoubleGradient) rounds to the next multiple of 2;
153    /// * [`Blockhash`](enum.HashAlg.html#variant.Blockhash) rounds to the next multiple of 4.
154    ///
155    /// If the chosen values already satisfy these requirements then nothing is changed.
156    ///
157    /// ### Recommended Values
158    /// The hash granularity increases with `width * height`, although there are diminishing
159    /// returns for higher values. Start small. A good starting value to try is `8, 8`.
160    ///
161    /// When using DCT preprocessing having `width` and `height` be the same value will improve
162    /// hashing performance as only one set of coefficients needs to be used.
163    pub fn hash_size(self, width: u32, height: u32) -> Self {
164        Self {
165            width,
166            height,
167            ..self
168        }
169    }
170
171    /// Set the filter used to resize images during hashing.
172    ///
173    /// Note when picking a filter that images are almost always reduced in size.
174    /// Has no effect with the Blockhash algorithm as it does not resize.
175    pub fn resize_filter(self, resize_filter: FilterType) -> Self {
176        Self {
177            resize_filter,
178            ..self
179        }
180    }
181
182    /// Set the algorithm used to generate hashes.
183    ///
184    /// Each algorithm has different performance characteristics.
185    pub fn hash_alg(self, hash_alg: HashAlg) -> Self {
186        Self { hash_alg, ..self }
187    }
188
189    /// Enable preprocessing with the Discrete Cosine Transform (DCT).
190    ///
191    /// Does nothing when used with [the Blockhash.io algorithm](HashAlg::Blockhash)
192    /// which does not scale the image.
193    /// (RFC: it would be possible to shoehorn a DCT into the Blockhash algorithm but it's
194    /// not clear what benefits, if any, that would provide).
195    ///
196    /// After conversion to grayscale, the image is scaled down to `width * 2 x height * 2`
197    /// and then the Discrete Cosine Transform is performed on the luminance values. The DCT
198    /// essentially transforms the 2D image from the spatial domain with luminance values
199    /// to a 2D frequency domain where the values are amplitudes of cosine waves. The resulting
200    /// 2D matrix is then cropped to the low `width * height` corner and the
201    /// configured hash algorithm is performed on that.
202    ///
203    /// In layman's terms, this essentially converts the image into a mathematical representation
204    /// of the "broad strokes" of the data, which allows the subsequent hashing step to be more
205    /// robust against changes that may otherwise produce different hashes, such as significant
206    /// edits to portions of the image.
207    ///
208    /// However, on most machines this usually adds an additional 50-100% to the average hash time.
209    ///
210    /// This is a very similar process to JPEG compression, although the implementation is too
211    /// different for this to be optimized specifically for JPEG encoded images.
212    ///
213    /// Further Reading:
214    /// * http://www.hackerfactor.com/blog/?/archives/432-Looks-Like-It.html
215    /// Krawetz describes a "pHash" algorithm which is equivalent to Mean + DCT preprocessing here.
216    /// However there is nothing to say that DCT preprocessing cannot compose with other hash
217    /// algorithms; Gradient + DCT might well perform better in some aspects.
218    /// * https://en.wikipedia.org/wiki/Discrete_cosine_transform
219    pub fn preproc_dct(self) -> Self {
220        Self { dct: true, ..self }
221    }
222
223    /// Enable preprocessing with the Difference of Gaussians algorithm with default sigma values.
224    ///
225    /// Recommended only for use with [the Blockhash.io algorithm](enum.HashAlg#variant.Blockhash)
226    /// as it significantly reduces entropy in the scaled down image for other algorithms.
227    ///
228    /// See [`Self::preproc_diff_gauss_sigmas()](#method.preproc_diff_gauss_sigmas) for more info.
229    pub fn preproc_diff_gauss(self) -> Self {
230        self.preproc_diff_gauss_sigmas(5.0, 10.0)
231    }
232
233    /// Enable preprocessing with the Difference of Gaussians algorithm with the given sigma values.
234    ///
235    /// Recommended only for use with [the Blockhash.io algorithm](enum.HashAlg#variant.Blockhash)
236    /// as it significantly reduces entropy in the scaled down image for other algorithms.
237    ///
238    /// After the image is converted to grayscale, it is blurred with a Gaussian blur using
239    /// two different sigmas, and then the images are subtracted from each other. This reduces
240    /// the image to just sharp transitions in luminance, i.e. edges. Varying the sigma values
241    /// changes how sharp the edges are^[citation needed].
242    ///
243    /// Further reading:
244    /// * https://en.wikipedia.org/wiki/Difference_of_Gaussians
245    /// * http://homepages.inf.ed.ac.uk/rbf/HIPR2/log.htm
246    /// (Difference of Gaussians is an approximation of a Laplacian of Gaussian filter)
247    pub fn preproc_diff_gauss_sigmas(self, sigma_a: f32, sigma_b: f32) -> Self {
248        Self {
249            gauss_sigmas: Some([sigma_a, sigma_b]),
250            ..self
251        }
252    }
253
254    /// Create a [`Hasher`](struct.Hasher.html) from this config which can be used to hash images.
255    ///
256    /// ### Panics
257    /// If the chosen hash size (`width x height`, rounded for the algorithm if necessary)
258    /// is too large for the chosen container type (`B::max_bits()`).
259    pub fn to_hasher(&self) -> Hasher<B> {
260        let Self {
261            hash_alg,
262            width,
263            height,
264            gauss_sigmas,
265            resize_filter,
266            dct,
267            ..
268        } = *self;
269
270        let (width, height) = hash_alg.round_hash_size(width, height);
271
272        assert!(
273            (width * height) as usize <= B::max_bits(),
274            "hash size too large for container: {} x {}",
275            width,
276            height
277        );
278
279        // Blockhash doesn't resize the image so don't waste time calculating coefficients
280        let dct_coeffs = if dct && hash_alg != HashAlg::Blockhash {
281            // calculate the coefficients based on the resize dimensions
282            let (dct_width, dct_height) = hash_alg.resize_dimensions(width, height);
283            Some(DctCtxt::new(dct_width, dct_height))
284        } else {
285            None
286        };
287
288        Hasher {
289            ctxt: HashCtxt {
290                gauss_sigmas,
291                dct_ctxt: dct_coeffs,
292                width,
293                height,
294                resize_filter,
295            },
296            hash_alg,
297            bytes_type: PhantomData,
298        }
299    }
300}
301
302// cannot be derived because of `FilterType`
303impl<B> fmt::Debug for HasherConfig<B> {
304    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
305        f.debug_struct("HasherConfig")
306            .field("width", &self.width)
307            .field("height", &self.height)
308            .field("hash_alg", &self.hash_alg)
309            .field("resize_filter", &debug_filter_type(&self.resize_filter))
310            .field("gauss_sigmas", &self.gauss_sigmas)
311            .field("use_dct", &self.dct)
312            .finish()
313    }
314}
315
316impl Default for HasherConfig {
317    fn default() -> Self {
318        HasherConfig::new()
319    }
320}
321
322/// Generates hashes for images.
323///
324/// Constructed via [`HasherConfig::to_hasher()`](struct.HasherConfig#method.to_hasher).
325pub struct Hasher<B = Box<[u8]>> {
326    ctxt: HashCtxt,
327    hash_alg: HashAlg,
328    bytes_type: PhantomData<B>,
329}
330
331impl<B> Hasher<B>
332where
333    B: HashBytes,
334{
335    /// Calculate a hash for the given image with the configured options.
336    pub fn hash_image<I: Image>(&self, img: &I) -> ImageHash<B> {
337        let hash = self.hash_alg.hash_image(&self.ctxt, img);
338        ImageHash { hash }
339    }
340}
341
342enum CowImage<'a, I: Image> {
343    Borrowed(&'a I),
344    Owned(I::Buf),
345}
346
347impl<'a, I: Image> CowImage<'a, I> {
348    fn to_grayscale(&self) -> Cow<GrayImage> {
349        match self {
350            CowImage::Borrowed(img) => img.to_grayscale(),
351            CowImage::Owned(img) => img.to_grayscale(),
352        }
353    }
354}
355
356enum HashVals {
357    Floats(Vec<f32>),
358    Bytes(Vec<u8>),
359}
360
361// TODO: implement `Debug`, needs adaptor for `FilterType`
362struct HashCtxt {
363    gauss_sigmas: Option<[f32; 2]>,
364    dct_ctxt: Option<DctCtxt>,
365    resize_filter: FilterType,
366    width: u32,
367    height: u32,
368}
369
370impl HashCtxt {
371    /// If Difference of Gaussians preprocessing is configured, produce a new image with it applied.
372    fn gauss_preproc<'a, I: Image>(&self, image: &'a I) -> CowImage<'a, I> {
373        if let Some([sigma_a, sigma_b]) = self.gauss_sigmas {
374            let mut blur_a = image.blur(sigma_a);
375            let blur_b = image.blur(sigma_b);
376            blur_a.diff_inplace(&blur_b);
377
378            CowImage::Owned(blur_a)
379        } else {
380            CowImage::Borrowed(image)
381        }
382    }
383
384    /// If DCT preprocessing is configured, produce a vector of floats, otherwise a vector of bytes.
385    fn calc_hash_vals(&self, img: &GrayImage, width: u32, height: u32) -> HashVals {
386        if let Some(ref dct_ctxt) = self.dct_ctxt {
387            let img =
388                imageops::resize(img, dct_ctxt.width(), dct_ctxt.height(), self.resize_filter);
389
390            let img_vals = img.into_vec();
391            let mut packed_2d: Vec<_> = img_vals.iter().copied().map(f32::from).collect();
392            dct_ctxt.dct_2d(&mut packed_2d);
393            HashVals::Floats(dct_ctxt.crop_2d(packed_2d))
394        } else {
395            let img = imageops::resize(img, width, height, self.resize_filter);
396            HashVals::Bytes(img.into_vec())
397        }
398    }
399}
400
401/// A struct representing an image processed by a perceptual hash.
402/// For efficiency, does not retain a copy of the image data after hashing.
403///
404/// Get an instance with `ImageHash::hash()`.
405#[derive(PartialEq, Eq, Hash, Debug, Clone)]
406pub struct ImageHash<B = Box<[u8]>> {
407    hash: B,
408}
409
410impl<B: AsRef<[u8]>> ImageHash<B> {
411    /// Format this image has as a hex string.
412    pub fn to_hex(&self) -> String {
413        static CHARS: &[u8] = b"0123456789abcdef";
414
415        let bytes = self.hash.as_ref();
416        let mut v = Vec::with_capacity(bytes.len() * 2);
417        for &byte in bytes {
418            v.push(CHARS[(byte >> 4) as usize]);
419            v.push(CHARS[(byte & 0xf) as usize]);
420        }
421
422        unsafe { String::from_utf8_unchecked(v) }
423    }
424}
425
426/// Error that can happen constructing a `ImageHash` from bytes.
427#[derive(Debug, PartialEq)]
428pub enum InvalidBytesError {
429    /// Byte slice passed to `from_bytes` was the wrong length.
430    BytesWrongLength {
431        /// Number of bytes the `ImageHash` type expected.
432        expected: usize,
433        /// Number of bytes found when parsing the hash bytes.
434        found: usize,
435    },
436    /// String passed was not valid base64.
437    Base64(base64::DecodeError),
438}
439
440impl<B: HashBytes> ImageHash<B> {
441    /// Get the bytes of this hash.
442    pub fn as_bytes(&self) -> &[u8] {
443        self.hash.as_slice()
444    }
445
446    /// Create an `ImageHash` instance from the given bytes.
447    ///
448    /// ## Errors:
449    /// Returns a `InvalidBytesError::BytesWrongLength` error if the slice passed can't fit in `B`.
450    pub fn from_bytes(bytes: &[u8]) -> Result<ImageHash<B>, InvalidBytesError> {
451        if bytes.len() * 8 > B::max_bits() {
452            return Err(InvalidBytesError::BytesWrongLength {
453                expected: B::max_bits() / 8,
454                found: bytes.len(),
455            });
456        }
457
458        Ok(ImageHash {
459            hash: B::from_iter(bytes.iter().copied()),
460        })
461    }
462
463    /// Calculate the Hamming distance between this and `other`.
464    ///
465    /// Equivalent to counting the 1-bits of the XOR of the two hashes.
466    ///
467    /// Essential to determining the perceived difference between `self` and `other`.
468    ///
469    /// ### Note
470    /// This return value is meaningless if these two hashes are from different hash sizes or
471    /// algorithms.
472    pub fn dist(&self, other: &Self) -> u32 {
473        BitSet::hamming(&self.hash, &other.hash)
474    }
475
476    /// Create an `ImageHash` instance from the given Base64-encoded string.
477    ///
478    /// ## Errors:
479    /// Returns `InvaidBytesError::Base64(DecodeError::InvalidLength)` if the string wasn't valid base64`.
480    /// Otherwise returns the same errors as `from_bytes`.
481    pub fn from_base64(encoded_hash: &str) -> Result<ImageHash<B>, InvalidBytesError> {
482        let bytes = base64::decode(encoded_hash).map_err(InvalidBytesError::Base64)?;
483
484        Self::from_bytes(&bytes)
485    }
486
487    /// Get a Base64 string representing the bits of this hash.
488    ///
489    /// Mostly for printing convenience.
490    pub fn to_base64(&self) -> String {
491        base64::encode(self.hash.as_slice())
492    }
493}
494
495/// Provide Serde a typedef for `image::FilterType`: https://serde.rs/remote-derive.html
496/// This is automatically checked, if Serde complains then double-check with the original definition
497#[derive(Serialize, Deserialize)]
498#[serde(remote = "FilterType")]
499enum SerdeFilterType {
500    Nearest,
501    Triangle,
502    CatmullRom,
503    Gaussian,
504    Lanczos3,
505}
506
507fn debug_filter_type(ft: &FilterType) -> &'static str {
508    match ft {
509        FilterType::Triangle => "Triangle",
510        FilterType::Nearest => "Nearest",
511        FilterType::CatmullRom => "CatmullRom",
512        FilterType::Lanczos3 => "Lanczos3",
513        FilterType::Gaussian => "Gaussian",
514    }
515}