-
Notifications
You must be signed in to change notification settings - Fork 150
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
9 changed files
with
601 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
pub(super) struct Bits64(u64); | ||
|
||
impl Bits64 { | ||
const N: usize = 64; | ||
|
||
pub fn new(v: impl IntoIterator<Item = bool>) -> Self { | ||
let mut bits = Self::zeros(); | ||
let mut n = 0; | ||
for bit in v { | ||
if bit { | ||
bits.set_bit_at(n); | ||
} else { | ||
bits.unset_bit_at(n); | ||
}; | ||
n += 1; | ||
} | ||
assert_eq!(n, Self::N); | ||
bits | ||
} | ||
pub fn zeros() -> Self { | ||
Self(0) | ||
} | ||
#[allow(dead_code)] | ||
pub fn to_bitarray(self) -> [bool; Self::N] { | ||
let mut bits = [false; Self::N]; | ||
for (n, bit) in bits.iter_mut().enumerate() { | ||
*bit = self.bit_at(n) | ||
} | ||
bits | ||
} | ||
pub fn hamming_distance(self, other: Bits64) -> u32 { | ||
self.xor(other).0.count_ones() | ||
} | ||
fn xor(self, other: Self) -> Self { | ||
Self(self.0 ^ other.0) | ||
} | ||
fn bit_at(self, n: usize) -> bool { | ||
assert!(n < Self::N); | ||
let bit = self.0 & (1 << n); | ||
bit != 0 | ||
} | ||
fn set_bit_at(&mut self, n: usize) { | ||
assert!(n < Self::N); | ||
self.0 |= 1 << n; | ||
} | ||
fn unset_bit_at(&mut self, n: usize) { | ||
assert!(n < Self::N); | ||
self.0 &= !(1 << n); | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::collections::HashMap; | ||
|
||
use super::*; | ||
|
||
#[test] | ||
fn test_bits64_ops() { | ||
let mut bits = Bits64::zeros(); | ||
bits.set_bit_at(0); | ||
assert_eq!(bits, Bits64(1)); | ||
bits.set_bit_at(1); | ||
assert_eq!(bits, Bits64(1 + 2)); | ||
bits.unset_bit_at(0); | ||
assert_eq!(bits, Bits64(2)); | ||
bits.unset_bit_at(1); | ||
assert_eq!(bits, Bits64::zeros()); | ||
|
||
bits.set_bit_at(2); | ||
assert_eq!(bits, Bits64(4)); | ||
} | ||
#[test] | ||
fn test_bitarray() { | ||
let mut v = [false; Bits64::N]; | ||
v[3] = true; | ||
v[6] = true; | ||
let bits = Bits64::new(v); | ||
assert_eq!(bits.to_bitarray(), v); | ||
} | ||
#[test] | ||
fn test_bits64_new() { | ||
const N: usize = 64; | ||
|
||
let mut v = [false; N]; | ||
v[0] = true; | ||
assert_eq!(Bits64::new(v), Bits64(1)); | ||
v[1] = true; | ||
assert_eq!(Bits64::new(v), Bits64(1 + 2)); | ||
} | ||
#[test] | ||
#[should_panic] | ||
fn test_bits64_new_fail() { | ||
const N: usize = 64; | ||
let it = (1..N).map(|x| x % 2 == 0); | ||
let _bits = Bits64::new(it); | ||
} | ||
|
||
#[test] | ||
fn test_hash() { | ||
let one = Bits64(1); | ||
let map = HashMap::from([(one, "1")]); | ||
assert_eq!(map[&one], "1"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
//! [Perceptual hashing] algorithms for images. | ||
//! | ||
//! [Perceptual hashing]: https://en.wikipedia.org/wiki/Perceptual_hashing | ||
mod bits; | ||
mod phash; | ||
mod signals; | ||
|
||
use bits::Bits64; | ||
|
||
pub use phash::{phash, PHash}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
use super::{signals, Bits64}; | ||
use crate::definitions::Image; | ||
use image::{imageops, math::Rect, Luma}; | ||
use std::borrow::Cow; | ||
|
||
/// Stores the result of the [`phash`]. | ||
/// Implements [`Hash`] trait. | ||
/// | ||
/// # Note | ||
/// The hash value may vary between versions. | ||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
pub struct PHash(Bits64); | ||
|
||
impl PHash { | ||
/// Compute the [hamming distance] between hashes. | ||
/// | ||
/// # Example | ||
/// ```no_run | ||
/// use imageproc::image_hash; | ||
/// | ||
/// # fn main() { | ||
/// # let img1 = image::open("first.png").unwrap().to_luma32f(); | ||
/// # let img2 = image::open("second.png").unwrap().to_luma32f(); | ||
/// let hash1 = image_hash::phash(&img1); | ||
/// let hash2 = image_hash::phash(&img2); | ||
/// dbg!(hash1.hamming_distance(hash2)); | ||
/// # } | ||
/// ``` | ||
/// | ||
/// [hamming distance]: https://en.wikipedia.org/wiki/Hamming_distance | ||
pub fn hamming_distance(self, PHash(other): PHash) -> u32 { | ||
self.0.hamming_distance(other) | ||
} | ||
} | ||
|
||
/// Compute the [pHash] using [DCT]. | ||
/// | ||
/// # Example | ||
/// | ||
/// ```no_run | ||
/// use imageproc::image_hash; | ||
/// | ||
/// # fn main() { | ||
/// let img1 = image::open("first.png").unwrap().to_luma32f(); | ||
/// let img2 = image::open("second.png").unwrap().to_luma32f(); | ||
/// let hash1 = image_hash::phash(&img1); | ||
/// let hash2 = image_hash::phash(&img2); | ||
/// dbg!(hash1.hamming_distance(hash2)); | ||
/// # } | ||
/// ``` | ||
/// | ||
/// [pHash]: https://phash.org/docs/pubs/thesis_zauner.pdf | ||
/// [DCT]: https://en.wikipedia.org/wiki/Discrete_cosine_transform | ||
pub fn phash(img: &Image<Luma<f32>>) -> PHash { | ||
const N: u32 = 8; | ||
const HASH_FACTOR: u32 = 4; | ||
let img = imageops::resize( | ||
img, | ||
HASH_FACTOR * N, | ||
HASH_FACTOR * N, | ||
imageops::FilterType::Lanczos3, | ||
); | ||
let dct = signals::dct2d(Cow::Owned(img)); | ||
let topleft = Rect { | ||
x: 1, | ||
y: 1, | ||
width: N, | ||
height: N, | ||
}; | ||
let topleft_dct = crate::compose::crop(&dct, topleft); | ||
debug_assert_eq!(topleft_dct.dimensions(), (N, N)); | ||
assert_eq!(topleft_dct.len(), (N * N) as usize); | ||
let mean = | ||
topleft_dct.iter().copied().reduce(|a, b| a + b).unwrap() / (topleft_dct.len() as f32); | ||
let bits = topleft_dct.iter().map(|&x| x > mean); | ||
PHash(Bits64::new(bits)) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
#[test] | ||
fn test_phash() { | ||
let img1 = gray_image!(type: f32, | ||
1., 2., 3.; | ||
4., 5., 6. | ||
); | ||
let mut img2 = img1.clone(); | ||
*img2.get_pixel_mut(0, 0) = Luma([0f32]); | ||
let mut img3 = img2.clone(); | ||
*img3.get_pixel_mut(0, 1) = Luma([0f32]); | ||
|
||
let hash1 = phash(&img1); | ||
let hash2 = phash(&img2); | ||
let hash3 = phash(&img3); | ||
|
||
assert_eq!(0, hash1.hamming_distance(hash1)); | ||
assert_eq!(0, hash2.hamming_distance(hash2)); | ||
assert_eq!(0, hash3.hamming_distance(hash3)); | ||
|
||
assert_eq!(hash1.hamming_distance(hash2), hash2.hamming_distance(hash1)); | ||
|
||
assert!(hash1.hamming_distance(hash2) > 0); | ||
assert!(hash1.hamming_distance(hash3) > 0); | ||
assert!(hash2.hamming_distance(hash3) > 0); | ||
|
||
assert!(hash1.hamming_distance(hash2) < hash1.hamming_distance(hash3)); | ||
} | ||
} | ||
|
||
#[cfg(not(miri))] | ||
#[cfg(test)] | ||
mod proptests { | ||
use super::*; | ||
use crate::proptest_utils::arbitrary_image; | ||
use proptest::prelude::*; | ||
|
||
const N: usize = 100; | ||
|
||
proptest! { | ||
#[test] | ||
fn proptest_phash(img in arbitrary_image(0..N, 0..N)) { | ||
let hash = phash(&img); | ||
assert_eq!(0, hash.hamming_distance(hash)); | ||
} | ||
} | ||
} | ||
|
||
#[cfg(not(miri))] | ||
#[cfg(test)] | ||
mod benches { | ||
use super::*; | ||
use crate::utils::luma32f_bench_image; | ||
use test::{black_box, Bencher}; | ||
|
||
const N: u32 = 600; | ||
|
||
#[bench] | ||
fn bench_phash(b: &mut Bencher) { | ||
let img = luma32f_bench_image(N, N); | ||
b.iter(|| { | ||
let img = black_box(&img); | ||
black_box(phash(img)); | ||
}); | ||
} | ||
} |
Oops, something went wrong.