You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
244 lines
7.3 KiB
244 lines
7.3 KiB
// Copyright 2016 Drew Short <drew@sothr.com>.
|
|
//
|
|
// Licensed under the MIT license<LICENSE-MIT or http://opensource.org/licenses/MIT>.
|
|
// This file may not be copied, modified, or distributed except according to those terms.
|
|
|
|
extern crate dft;
|
|
extern crate image;
|
|
|
|
mod ahash;
|
|
mod dhash;
|
|
mod phash;
|
|
|
|
use std::path::Path;
|
|
use std::f64;
|
|
use self::image::{Pixel, FilterType};
|
|
use cache::Cache;
|
|
|
|
// Constants //
|
|
|
|
// Used to get ranges for the precision of rounding floats
|
|
// Can round to 1 significant factor of precision
|
|
const FLOAT_PRECISION_MAX_1: f64 = f64::MAX / 10_f64;
|
|
const FLOAT_PRECISION_MIN_1: f64 = f64::MIN / 10_f64;
|
|
// Can round to 2 significant factors of precision
|
|
const FLOAT_PRECISION_MAX_2: f64 = f64::MAX / 100_f64;
|
|
const FLOAT_PRECISION_MIN_2: f64 = f64::MIN / 100_f64;
|
|
// Can round to 3 significant factors of precision
|
|
const FLOAT_PRECISION_MAX_3: f64 = f64::MAX / 1000_f64;
|
|
const FLOAT_PRECISION_MIN_3: f64 = f64::MIN / 1000_f64;
|
|
// Can round to 4 significant factors of precision
|
|
const FLOAT_PRECISION_MAX_4: f64 = f64::MAX / 10000_f64;
|
|
const FLOAT_PRECISION_MIN_4: f64 = f64::MIN / 10000_f64;
|
|
// Can round to 5 significant factors of precision
|
|
const FLOAT_PRECISION_MAX_5: f64 = f64::MAX / 100000_f64;
|
|
const FLOAT_PRECISION_MIN_5: f64 = f64::MIN / 100000_f64;
|
|
// Hamming Distance Similarity Limit //
|
|
const HAMMING_DISTANCE_SIMILARITY_LIMIT: u64 = 5u64;
|
|
|
|
// Structs/Enums //
|
|
|
|
/**
|
|
* Prepared image that can be used to generate hashes
|
|
*/
|
|
pub struct PreparedImage<'a> {
|
|
orig_path: &'a str,
|
|
image: Option<image::ImageBuffer<image::Luma<u8>, Vec<u8>>>,
|
|
}
|
|
|
|
/**
|
|
* Wraps the various perceptual hashes
|
|
*/
|
|
pub struct PerceptualHashes<'a> {
|
|
pub orig_path: &'a str,
|
|
pub ahash: u64,
|
|
pub dhash: u64,
|
|
pub phash: u64,
|
|
}
|
|
|
|
impl<'a> PerceptualHashes<'a> {
|
|
pub fn similar(&self, other: &'a PerceptualHashes<'a>) -> bool {
|
|
if self.orig_path != other.orig_path &&
|
|
calculate_hamming_distance(self.ahash, other.ahash) <= HAMMING_DISTANCE_SIMILARITY_LIMIT &&
|
|
calculate_hamming_distance(self.dhash, other.dhash) <= HAMMING_DISTANCE_SIMILARITY_LIMIT &&
|
|
calculate_hamming_distance(self.phash, other.phash) <= HAMMING_DISTANCE_SIMILARITY_LIMIT {
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* All the supported precision types
|
|
*
|
|
* Low aims for 32 bit precision
|
|
* Medium aims for 64 bit precision
|
|
* High aims for 128 bit precision
|
|
*/
|
|
#[allow(dead_code)]
|
|
pub enum Precision {
|
|
Low,
|
|
Medium,
|
|
High,
|
|
}
|
|
|
|
// Get the size of the required image
|
|
//
|
|
impl Precision {
|
|
fn get_size(&self) -> u32 {
|
|
match *self {
|
|
Precision::Low => 4,
|
|
Precision::Medium => 8,
|
|
Precision::High => 16,
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Types of hashes supported
|
|
*/
|
|
pub enum HashType {
|
|
AHash,
|
|
DHash,
|
|
PHash,
|
|
}
|
|
|
|
// Traits //
|
|
|
|
pub trait PerceptualHash {
|
|
fn get_hash(&self, cache: &Option<Cache>) -> u64;
|
|
}
|
|
|
|
// Functions //
|
|
|
|
/**
|
|
* Resonsible for parsing a path, converting an image and package it to be
|
|
* hashed.
|
|
*
|
|
* # Arguments
|
|
*
|
|
* * 'path' - The path to the image requested to be hashed
|
|
* * 'size' - The size that the image should be resize to, in the form of size x size
|
|
*
|
|
* # Returns
|
|
*
|
|
* A PreparedImage struct with the required information for performing hashing
|
|
*
|
|
*/
|
|
pub fn prepare_image<'a>(path: &'a Path,
|
|
hash_type: &HashType,
|
|
precision: &Precision,
|
|
cache: &Option<Cache>)
|
|
-> PreparedImage<'a> {
|
|
let image_path = path.to_str().unwrap();
|
|
let size: u32 = match *hash_type {
|
|
HashType::PHash => precision.get_size() * 4,
|
|
_ => precision.get_size(),
|
|
};
|
|
// Check if we have the already converted image in a cache and use that if possible.
|
|
match *cache {
|
|
Some(ref c) => {
|
|
match c.get_image_from_cache(&path, size) {
|
|
Some(image) => {
|
|
PreparedImage {
|
|
orig_path: &*image_path,
|
|
image: Some(image),
|
|
}
|
|
}
|
|
None => {
|
|
let processed_image = process_image(&image_path, size);
|
|
// Oh, and save it in a cache
|
|
match processed_image.image {
|
|
Some(ref image) => {
|
|
match c.put_image_in_cache(&path, size, &image) {
|
|
Ok(_) => {}
|
|
Err(e) => println!("Unable to store image in cache. {}", e),
|
|
};
|
|
},
|
|
None => {}
|
|
};
|
|
processed_image
|
|
}
|
|
}
|
|
},
|
|
None => process_image(&image_path, size),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Turn the image into something we can work with
|
|
*/
|
|
fn process_image<'a>(image_path: &'a str,
|
|
size: u32) -> PreparedImage<'a> {
|
|
// Otherwise let's do that work now and store it.
|
|
//println!("Path: {}", image_path);
|
|
let image = match image::open(Path::new(image_path)) {
|
|
Ok(image) => {
|
|
let small_image = image.resize_exact(size, size, FilterType::Lanczos3);
|
|
Some(small_image.to_luma())
|
|
},
|
|
Err(e) => {
|
|
println!("Error Processing Image [{}]: {} ", image_path, e);
|
|
None
|
|
}
|
|
};
|
|
PreparedImage {
|
|
orig_path: &*image_path,
|
|
image: image,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a specific HashType hash
|
|
*/
|
|
pub fn get_perceptual_hash<'a>(path: &'a Path,
|
|
precision: &Precision,
|
|
hash_type: &HashType,
|
|
cache: &Option<Cache>)
|
|
-> u64 {
|
|
match *hash_type {
|
|
HashType::AHash => ahash::AHash::new(&path, &precision, &cache).get_hash(&cache),
|
|
HashType::DHash => dhash::DHash::new(&path, &precision, &cache).get_hash(&cache),
|
|
HashType::PHash => phash::PHash::new(&path, &precision, &cache).get_hash(&cache),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all perceptual hashes for an image
|
|
*/
|
|
pub fn get_perceptual_hashes<'a>(path: &'a Path,
|
|
precision: &Precision,
|
|
cache: &Option<Cache>)
|
|
-> PerceptualHashes<'a> {
|
|
let image_path = path.to_str().unwrap();
|
|
let ahash = ahash::AHash::new(&path, &precision, &cache).get_hash(&cache);
|
|
let dhash = dhash::DHash::new(&path, &precision, &cache).get_hash(&cache);
|
|
let phash = phash::PHash::new(&path, &precision, &cache).get_hash(&cache);
|
|
PerceptualHashes {
|
|
orig_path: &*image_path,
|
|
ahash: ahash,
|
|
dhash: dhash,
|
|
phash: phash,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate the number of bits different between two hashes
|
|
* Add to the PerceptualHashTrait
|
|
*/
|
|
pub fn calculate_hamming_distance(hash1: u64, hash2: u64) -> u64 {
|
|
// The binary xor of the two hashes should give us a number representing
|
|
// the differences between the two hashes. All that's left is to count
|
|
// the number of 1's in the difference to determine the hamming distance
|
|
let bin_diff = hash1 ^ hash2;
|
|
let bin_diff_str = format!("{:b}", bin_diff);
|
|
let mut hamming = 0u64;
|
|
for bit in bin_diff_str.chars() {
|
|
match bit {
|
|
'1' => hamming += 1,
|
|
_ => continue,
|
|
}
|
|
}
|
|
hamming
|
|
}
|