use crate::{
imageops::ImageOps,
math::{dct2_over_matrix_in_place, median, Axis},
ColorSpace, ImageHash, ImageHasher,
};
#[derive(Debug, Clone)]
pub struct PerceptualHasher {
pub width: u32,
pub height: u32,
pub factor: u32,
pub color_space: ColorSpace,
}
impl ImageHasher for PerceptualHasher {
fn hash_from_img(&self, img: &image::DynamicImage) -> ImageHash {
let width = self.width * self.factor;
let height = self.height * self.factor;
let high_freq = self.convert(img, width, height, self.color_space);
let mut dct_matrix = high_freq
.as_bytes()
.iter()
.copied()
.map(|v| v as f64)
.collect::<Vec<_>>();
dct2_over_matrix_in_place(&mut dct_matrix, width as usize, Axis::Column);
dct2_over_matrix_in_place(&mut dct_matrix, width as usize, Axis::Row);
let scaled_matrix = dct_matrix
.chunks(width as usize)
.take(self.height as usize)
.flat_map(|row| &row[0..self.width as usize])
.copied()
.collect::<Vec<_>>();
let median = median(scaled_matrix.iter().copied()).unwrap();
ImageHash::from_bool_iter(
scaled_matrix.into_iter().map(|pixel| pixel > median),
self.width,
self.height,
)
}
}
impl Default for PerceptualHasher {
fn default() -> Self {
PerceptualHasher {
width: 8,
height: 8,
factor: 4,
color_space: ColorSpace::REC601,
}
}
}
impl ImageOps for PerceptualHasher {}
#[cfg(test)]
mod tests {
use std::path::Path;
use image::ImageReader;
use super::*;
const TEST_IMG: &str = "./data/img/test.png";
const TXT_FILE: &str = "./data/misc/test.txt";
const REC_601_HASH: &str = "acdbe86135344e3a";
const REC_709_HASH: &str = "acdbe86135344e3a";
#[test]
fn test_perceptual_hash_from_img() {
let img = ImageReader::open(Path::new(TEST_IMG))
.unwrap()
.decode()
.unwrap();
let hasher = PerceptualHasher {
..Default::default()
};
let hash = hasher.hash_from_img(&img);
assert_eq!(hash.encode(), REC_601_HASH)
}
#[test]
fn test_perceptual_hash_from_img_with_rec_709() {
let img = ImageReader::open(Path::new(TEST_IMG))
.unwrap()
.decode()
.unwrap();
let hasher = PerceptualHasher {
color_space: ColorSpace::REC709,
..Default::default()
};
let hash = hasher.hash_from_img(&img);
assert_eq!(hash.encode(), REC_709_HASH)
}
#[test]
fn test_perceptual_hash_from_path() {
let hasher = PerceptualHasher {
..Default::default()
};
let hash = hasher.hash_from_path(Path::new(TEST_IMG));
match hash {
Ok(hash) => assert_eq!(hash.encode(), REC_601_HASH),
Err(err) => panic!("could not read image: {:?}", err),
}
}
#[test]
fn test_perceptual_hash_from_nonexisting_path() {
let hasher = PerceptualHasher {
..Default::default()
};
let hash = hasher.hash_from_path(Path::new("./does/not/exist.png"));
match hash {
Ok(hash) => panic!("found hash for non-existing image: {:?}", hash),
Err(_) => (),
}
}
#[test]
fn test_perceptual_hash_from_txt_file() {
let hasher = PerceptualHasher {
..Default::default()
};
let hash = hasher.hash_from_path(Path::new(TXT_FILE));
match hash {
Ok(hash) => panic!("found hash for non-existing image: {:?}", hash),
Err(_) => (),
}
}
}