Compare commits

...

9 Commits

  1. 238
      .gitlab-ci.yml
  2. 2
      Cargo.toml
  3. 12
      src/cache.rs
  4. 5
      src/hash/ahash.rs
  5. 10
      src/hash/mod.rs
  6. 49
      src/hash/phash.rs
  7. 81
      src/lib.rs
  8. 7
      src/main.rs

238
.gitlab-ci.yml

@ -1,42 +1,260 @@
stages: stages:
- build
- test - test
- release
test stable:
build:linux:stable:
image: scorpil/rust:stable
stage: build
script:
- rustc -Vv
- cargo -Vv
- cargo build
only:
- master
- develop
tags:
- docker
artifacts:
expire_in: 4 hours
paths:
- target/debug/
allow_failure: false
test:linux:stable:
image: scorpil/rust:stable image: scorpil/rust:stable
stage: test stage: test
script: script:
- rustc -V
- cargo -V
- rustc -Vv
- cargo -Vv
- cargo test - cargo test
only: only:
- master - master
- develop
tags: tags:
- docker - docker
dependencies:
- build:linux:stable
allow_failure: false allow_failure: false
test beta:
release:linux:stable:
image: scorpil/rust:stable
stage: release
script:
- rustc -Vv
- cargo -Vv
- cargo build --release
artifacts:
expire_in: 4 weeks
paths:
- target/release/*.so
- target/release/pihash
only:
- master
- develop
tags:
- docker
allow_failure: false
build:linux:beta:
image: scorpil/rust:beta
stage: build
script:
- rustc -Vv
- cargo -Vv
- cargo build
only:
- master
- develop
tags:
- docker
artifacts:
expire_in: 4 hours
paths:
- target/debug/
allow_failure: true
test:linux:beta:
image: scorpil/rust:beta image: scorpil/rust:beta
stage: test stage: test
script: script:
- rustc -V
- cargo -V
- rustc -Vv
- cargo -Vv
- cargo test - cargo test
only: only:
- master - master
- develop
tags:
- docker
dependencies:
- build:linux:beta
allow_failure: true
build:linux:nightly:
image: scorpil/rust:nightly
stage: build
script:
- rustc -Vv
- cargo -Vv
- cargo build
only:
- develop
tags: tags:
- docker - docker
artifacts:
expire_in: 4 hours
paths:
- target/debug/
allow_failure: true allow_failure: true
test nightly:
test:linux:nightly:
image: scorpil/rust:nightly image: scorpil/rust:nightly
stage: test stage: test
script: script:
- rustc -V
- cargo -V
- rustc -Vv
- cargo -Vv
- cargo test - cargo test
only: only:
- master
- develop
tags: tags:
- docker - docker
dependencies:
- build:linux:nightly
allow_failure: true allow_failure: true
release:linux:nightly:
image: scorpil/rust:nightly
stage: release
script:
- rustc -Vv
- cargo -Vv
- cargo build --release
artifacts:
expire_in: 1 week
paths:
- target/release/*.so
- target/release/pihash
only:
- develop
tags:
- docker
allow_failure: true
build:windows:stable:
stage: build
variables:
RUST_TOOLCHAIN: stable-x86_64-pc-windows-gnu
script:
- rustup update
- rustup run %RUST_TOOLCHAIN% rustc -Vv
- rustup run %RUST_TOOLCHAIN% cargo -Vv
- rustup run %RUST_TOOLCHAIN% cargo build
artifacts:
expire_in: 4 hours
paths:
- target/debug/
only:
- master
- develop
tags:
- rust
- windows
allow_failure: false
test:windows:stable:
stage: test
variables:
RUST_TOOLCHAIN: stable-x86_64-pc-windows-gnu
script:
- rustup update
- rustup run %RUST_TOOLCHAIN% rustc -Vv
- rustup run %RUST_TOOLCHAIN% cargo -Vv
- rustup run %RUST_TOOLCHAIN% cargo test
only:
- master
- develop
tags:
- rust
- windows
dependencies:
- build:windows:stable
allow_failure: false
release:windows:stable:
stage: release
variables:
RUST_TOOLCHAIN: stable-x86_64-pc-windows-gnu
script:
- rustup run %RUST_TOOLCHAIN% rustc -Vv
- rustup run %RUST_TOOLCHAIN% cargo -Vv
- rustup run %RUST_TOOLCHAIN% cargo build --release
artifacts:
expire_in: 4 weeks
paths:
- target/release/*.dll
- target/release/pihash.exe
only:
- master
- develop
tags:
- rust
- windows
allow_failure: false
build:windows:nightly:
stage: build
variables:
RUST_TOOLCHAIN: nightly-x86_64-pc-windows-gnu
script:
- rustup update
- rustup run %RUST_TOOLCHAIN% rustc -Vv
- rustup run %RUST_TOOLCHAIN% cargo -Vv
- rustup run %RUST_TOOLCHAIN% cargo build
artifacts:
expire_in: 4 hours
paths:
- target/debug/
only:
- develop
tags:
- rust
- windows
allow_failure: true
test:windows:nightly:
stage: test
variables:
RUST_TOOLCHAIN: nightly-x86_64-pc-windows-gnu
script:
- rustup update
- rustup run %RUST_TOOLCHAIN% rustc -Vv
- rustup run %RUST_TOOLCHAIN% cargo -Vv
- rustup run %RUST_TOOLCHAIN% cargo test
only:
- develop
tags:
- rust
- windows
dependencies:
- build:windows:nightly
allow_failure: true
release:windows:nightly:
stage: release
variables:
RUST_TOOLCHAIN: nightly-x86_64-pc-windows-gnu
script:
- rustup run %RUST_TOOLCHAIN% rustc -Vv
- rustup run %RUST_TOOLCHAIN% cargo -Vv
- rustup run %RUST_TOOLCHAIN% cargo build --release
artifacts:
expire_in: 1 week
paths:
- target/release/*.dll
- target/release/pihash.exe
only:
- develop
tags:
- rust
- windows
allow_failure: false

2
Cargo.toml

@ -1,6 +1,6 @@
[package] [package]
name = "pihash" name = "pihash"
version = "0.3.6"
version = "0.4.0"
authors = ["Drew Short <warrick@sothr.com>"] authors = ["Drew Short <warrick@sothr.com>"]
description = "A simple library for generating perceptual hashes for images and comparing images based on their perceptual hashes." description = "A simple library for generating perceptual hashes for images and comparing images based on their perceptual hashes."
repository = "https://github.com/warricksothr/Perceptual-Image-Hashing/" repository = "https://github.com/warricksothr/Perceptual-Image-Hashing/"

12
src/cache.rs

@ -134,9 +134,9 @@ impl<'a> Cache<'a> {
* Get the hash of the desired file and return it as a hex string * Get the hash of the desired file and return it as a hex string
*/ */
pub fn get_file_hash(&self, path: &Path) -> Result<String, Error> { pub fn get_file_hash(&self, path: &Path) -> Result<String, Error> {
let mut source = try!(File::open(&path));
let mut source = File::open(&path)?;
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
try!(source.read_to_end(&mut buf));
source.read_to_end(&mut buf)?;
let mut sha1 = Sha1::new(); let mut sha1 = Sha1::new();
sha1.update(&buf); sha1.update(&buf);
let digest = sha1.digest(); let digest = sha1.digest();
@ -261,7 +261,7 @@ impl<'a> Cache<'a> {
let desire_len = row_str.len() - 1; let desire_len = row_str.len() - 1;
row_str.truncate(desire_len); row_str.truncate(desire_len);
row_str.push_str("\n"); row_str.push_str("\n");
try!(compressor.write(&row_str.into_bytes()));
compressor.write(&row_str.into_bytes())?;
} }
let compressed_matrix = match compressor.finish() { let compressed_matrix = match compressor.finish() {
Ok(data) => data, Ok(data) => data,
@ -270,8 +270,8 @@ impl<'a> Cache<'a> {
return Err(e); return Err(e);
} }
}; };
try!(file.write(&compressed_matrix));
try!(file.flush());
file.write(&compressed_matrix)?;
file.flush()?;
} }
Err(e) => { Err(e) => {
return Err(e); return Err(e);
@ -355,7 +355,7 @@ fn test_get_file_hash() {
match hash { match hash {
Ok(v) => { Ok(v) => {
println!("Hash: {}", v); println!("Hash: {}", v);
assert!(v == "4beb6f2d852b75a313863916a1803ebad13a3196");
assert_eq!(v, "4beb6f2d852b75a313863916a1803ebad13a3196");
} }
Err(e) => { Err(e) => {
println!("Error: {:?}", e); println!("Error: {:?}", e);

5
src/hash/ahash.rs

@ -38,11 +38,9 @@ impl<'a> PerceptualHash for AHash<'a> {
let mut total = 0u64; let mut total = 0u64;
for pixel in image.pixels() { for pixel in image.pixels() {
let channels = pixel.channels(); let channels = pixel.channels();
// println!("Pixel is: {}", channels[0]);
total += channels[0] as u64; total += channels[0] as u64;
} }
let mean = total / (width * height) as u64; let mean = total / (width * height) as u64;
// println!("Mean for {} is {}", prepared_image.orig_path, mean);
// Calculating a hash based on the mean // Calculating a hash based on the mean
let mut hash = 0u64; let mut hash = 0u64;
@ -51,14 +49,11 @@ impl<'a> PerceptualHash for AHash<'a> {
let pixel_sum = channels[0] as u64; let pixel_sum = channels[0] as u64;
if pixel_sum >= mean { if pixel_sum >= mean {
hash |= 1; hash |= 1;
// println!("Pixel {} is >= {} therefore {:b}", pixel_sum, mean, hash);
} else { } else {
hash |= 0; hash |= 0;
// println!("Pixel {} is < {} therefore {:b}", pixel_sum, mean, hash);
} }
hash <<= 1; hash <<= 1;
} }
// println!("Hash for {} is {}", prepared_image.orig_path, hash);
hash hash
} }
None => 0u64, None => 0u64,

10
src/hash/mod.rs

@ -116,7 +116,7 @@ pub trait PerceptualHash {
// Functions // // Functions //
/** /**
* Resonsible for parsing a path, converting an image and package it to be
* Responsible for parsing a path, converting an image and package it to be
* hashed. * hashed.
* *
* # Arguments * # Arguments
@ -187,7 +187,7 @@ fn process_image<'a>(image_path: &'a str, size: u32) -> PreparedImage<'a> {
}; };
PreparedImage { PreparedImage {
orig_path: &*image_path, orig_path: &*image_path,
image: image,
image,
} }
} }
@ -219,9 +219,9 @@ pub fn get_perceptual_hashes<'a>(path: &'a Path,
let phash = phash::PHash::new(&path, &precision, &cache).get_hash(&cache); let phash = phash::PHash::new(&path, &precision, &cache).get_hash(&cache);
PerceptualHashes { PerceptualHashes {
orig_path: &*image_path, orig_path: &*image_path,
ahash: ahash,
dhash: dhash,
phash: phash,
ahash,
dhash,
phash,
} }
} }

49
src/hash/phash.rs

@ -41,7 +41,7 @@ impl<'a> PerceptualHash for PHash<'a> {
// Get 2d data to 2d FFT/DFT // Get 2d data to 2d FFT/DFT
// Either from the cache or calculate it // Either from the cache or calculate it
// Pretty fast already, so caching doesn't make a huge difference // Pretty fast already, so caching doesn't make a huge difference
// Atleast compared to opening and processing the images
// At least compared to opening and processing the images
let data_matrix: Vec<Vec<f64>> = match *cache { let data_matrix: Vec<Vec<f64>> = match *cache {
Some(ref c) => { Some(ref c) => {
match c.get_matrix_from_cache(&Path::new(self.prepared_image.orig_path), match c.get_matrix_from_cache(&Path::new(self.prepared_image.orig_path),
@ -116,11 +116,11 @@ fn create_data_matrix(width: usize,
data_matrix data_matrix
} }
// Use a 1D DFT to cacluate the 2D DFT.
// Use a 1D DFT to calculate the 2D DFT.
// //
// This is achieved by calculating the DFT for each row, then calculating the // This is achieved by calculating the DFT for each row, then calculating the
// DFT for each column of DFT row data. This means that a 32x32 image with have // DFT for each column of DFT row data. This means that a 32x32 image with have
// 1024 1D DFT operations performed on it. (Slightly caclulation intensive)
// 1024 1D DFT operations performed on it. (Slightly calculation intensive)
// //
// This operation is in place on the data in the provided vector // This operation is in place on the data in the provided vector
// //
@ -131,7 +131,6 @@ fn create_data_matrix(width: usize,
// http://calculator.vhex.net/post/calculator-result/2d-discrete-fourier-transform // http://calculator.vhex.net/post/calculator-result/2d-discrete-fourier-transform
// //
fn calculate_2d_dft(data_matrix: &mut Vec<Vec<f64>>) { fn calculate_2d_dft(data_matrix: &mut Vec<Vec<f64>>) {
// println!("{:?}", data_matrix);
let width = data_matrix.len(); let width = data_matrix.len();
let height = data_matrix[0].len(); let height = data_matrix[0].len();
@ -145,11 +144,9 @@ fn calculate_2d_dft(data_matrix: &mut Vec<Vec<f64>>) {
} }
// Perform the DCT on this column // Perform the DCT on this column
// println!("column[{}] before: {:?}", x, column);
let forward_plan = dft::Plan::new(dft::Operation::Forward, column.len()); let forward_plan = dft::Plan::new(dft::Operation::Forward, column.len());
column.transform(&forward_plan); column.transform(&forward_plan);
let complex_column = dft::unpack(&column); let complex_column = dft::unpack(&column);
// println!("column[{}] after: {:?}", x, complex_column);
complex_data_matrix.push(complex_column); complex_data_matrix.push(complex_column);
} }
@ -160,10 +157,8 @@ fn calculate_2d_dft(data_matrix: &mut Vec<Vec<f64>>) {
row.push(complex_data_matrix[x][y]); row.push(complex_data_matrix[x][y]);
} }
// Perform DCT on the row // Perform DCT on the row
// println!("row[{}] before: {:?}", y, row);
let forward_plan = dft::Plan::new(dft::Operation::Forward, row.len()); let forward_plan = dft::Plan::new(dft::Operation::Forward, row.len());
row.transform(&forward_plan); row.transform(&forward_plan);
// println!("row[{}] after: {:?}", y, row);
// Put the row values back // Put the row values back
for x in 0..width { for x in 0..width {
@ -209,23 +204,23 @@ fn test_2d_dft() {
println!("{:?}", test_matrix[2]); println!("{:?}", test_matrix[2]);
println!("{:?}", test_matrix[3]); println!("{:?}", test_matrix[3]);
assert!(test_matrix[0][0] == 24_f64);
assert!(test_matrix[0][1] == 0_f64);
assert!(test_matrix[0][2] == 0_f64);
assert!(test_matrix[0][3] == 0_f64);
assert!(test_matrix[1][0] == 0_f64);
assert!(test_matrix[1][1] == 0_f64);
assert!(test_matrix[1][2] == -2_f64);
assert!(test_matrix[1][3] == 2_f64);
assert!(test_matrix[2][0] == 0_f64);
assert!(test_matrix[2][1] == -2_f64);
assert!(test_matrix[2][2] == -4_f64);
assert!(test_matrix[2][3] == -2_f64);
assert!(test_matrix[3][0] == 0_f64);
assert!(test_matrix[3][1] == 2_f64);
assert!(test_matrix[3][2] == -2_f64);
assert!(test_matrix[3][3] == 0_f64);
assert_eq!(test_matrix[0][0], 24_f64);
assert_eq!(test_matrix[0][1], 0_f64);
assert_eq!(test_matrix[0][2], 0_f64);
assert_eq!(test_matrix[0][3], 0_f64);
assert_eq!(test_matrix[1][0], 0_f64);
assert_eq!(test_matrix[1][1], 0_f64);
assert_eq!(test_matrix[1][2], -2_f64);
assert_eq!(test_matrix[1][3], 2_f64);
assert_eq!(test_matrix[2][0], 0_f64);
assert_eq!(test_matrix[2][1], -2_f64);
assert_eq!(test_matrix[2][2], -4_f64);
assert_eq!(test_matrix[2][3], -2_f64);
assert_eq!(test_matrix[3][0], 0_f64);
assert_eq!(test_matrix[3][1], 2_f64);
assert_eq!(test_matrix[3][2], -2_f64);
assert_eq!(test_matrix[3][3], 0_f64);
} }

81
src/lib.rs

@ -14,6 +14,8 @@ extern crate test;
pub mod hash; pub mod hash;
pub mod cache; pub mod cache;
use std::collections::{HashMap, LinkedList};
use std::boxed::Box;
use std::path::Path; use std::path::Path;
use std::ffi::CStr; use std::ffi::CStr;
use cache::Cache; use cache::Cache;
@ -80,6 +82,50 @@ impl<'a> PIHash<'a> {
&hash::HashType::PHash, &hash::HashType::PHash,
&self.cache) &self.cache)
} }
pub fn get_all_similar_images(&self, images: &[&Path]) -> Option<HashMap<&Path, &Path>> {
for image in images {
println!("Test Path -> {}", image.to_str().unwrap());
}
None
}
pub fn get_similar_images(&self,
base_image: &Path,
images: &'a [&Path])
-> Option<LinkedList<&Path>> {
//let similar_images = LinkedList::new();
let similar_images = images
.iter()
.filter(|image| self.are_similar_images(&base_image, &image, None))
.map(|&x| x)
.collect::<LinkedList<&Path>>();
Some(similar_images)
}
pub fn are_similar_images(&self,
first: &Path,
second: &Path,
sensitivity: Option<u64>)
-> bool {
let threshold = match sensitivity {
Some(value) => value,
None => 4,
};
let first_pihash = self.get_phashes(first);
let second_pihash = self.get_phashes(second);
if hash::calculate_hamming_distance(first_pihash.ahash, second_pihash.ahash) <= threshold {
if hash::calculate_hamming_distance(first_pihash.dhash, second_pihash.dhash) <=
threshold {
if hash::calculate_hamming_distance(first_pihash.phash, second_pihash.phash) <=
threshold {
return true;
}
}
}
return false;
}
} }
/** /**
@ -182,7 +228,6 @@ fn to_hex_string(bytes: &[u8]) -> String {
// //
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use hash; use hash;
@ -255,6 +300,38 @@ mod tests {
} }
} }
#[test]
fn test_confirm_get_similar_images() {
let lib = PIHash::new(Some(cache::DEFAULT_CACHE_DIR));
let sample_01_base_image = &Path::new("./test_images/sample_01_large.jpg");
let sample_01_images: [&Path; 2] = [&Path::new("./test_images/sample_01_medium.jpg"),
&Path::new("./test_images/sample_01_small.jpg")];
let sample_01_results = lib.get_similar_images(&sample_01_base_image, &sample_01_images)
.unwrap();
assert_eq!(sample_01_results.len(), 2);
let sample_02_base_image = &Path::new("./test_images/sample_02_large.jpg");
let sample_02_images: [&Path; 2] = [&Path::new("./test_images/sample_02_medium.jpg"),
&Path::new("./test_images/sample_02_small.jpg")];
let sample_02_results = lib.get_similar_images(&sample_02_base_image, &sample_02_images)
.unwrap();
assert_eq!(sample_02_results.len(), 2);
let sample_03_base_image = &Path::new("./test_images/sample_03_large.jpg");
let sample_03_images: [&Path; 2] = [&Path::new("./test_images/sample_03_medium.jpg"),
&Path::new("./test_images/sample_03_small.jpg")];
let sample_03_results = lib.get_similar_images(&sample_03_base_image, &sample_03_images)
.unwrap();
assert_eq!(sample_03_results.len(), 2);
let sample_04_base_image = &Path::new("./test_images/sample_04_large.jpg");
let sample_04_images: [&Path; 2] = [&Path::new("./test_images/sample_04_medium.jpg"),
&Path::new("./test_images/sample_04_small.jpg")];
let sample_04_results = lib.get_similar_images(&sample_04_base_image, &sample_04_images)
.unwrap();
assert_eq!(sample_04_results.len(), 2);
}
#[test] #[test]
fn test_confirm_ahash_results() { fn test_confirm_ahash_results() {
// Prep_library // Prep_library
@ -451,7 +528,7 @@ mod tests {
let lib = PIHash::new(Some(cache::DEFAULT_CACHE_DIR)); let lib = PIHash::new(Some(cache::DEFAULT_CACHE_DIR));
// Setup the caches to make sure we're good to properly bench // Setup the caches to make sure we're good to properly bench
// All phashes so that the matricies are pulled from cache as well
// All phashes so that the matrices are pulled from cache as well
lib.get_perceptual_hash(&Path::new("./test_images/sample_01_large.jpg"), lib.get_perceptual_hash(&Path::new("./test_images/sample_01_large.jpg"),
&hash::Precision::Medium, &hash::Precision::Medium,
&hash::HashType::PHash); &hash::HashType::PHash);

7
src/main.rs

@ -82,7 +82,6 @@ fn main() {
for similar_image in similar_images { for similar_image in similar_images {
println!("{}", similar_image); println!("{}", similar_image);
} }
} else { } else {
let image_path = Path::new(&args.arg_path); let image_path = Path::new(&args.arg_path);
let hashes = get_requested_perceptual_hashes(&lib, &image_path, &args); let hashes = get_requested_perceptual_hashes(&lib, &image_path, &args);
@ -129,8 +128,8 @@ fn get_requested_perceptual_hashes<'a>(lib: &pihash::PIHash,
pihash::hash::PerceptualHashes { pihash::hash::PerceptualHashes {
orig_path: image_path.to_str().unwrap(), orig_path: image_path.to_str().unwrap(),
ahash: ahash,
dhash: dhash,
phash: phash,
ahash,
dhash,
phash,
} }
} }
Loading…
Cancel
Save