Calculates basic phashes for images and their mutations. More stats are needed, but it's a start
This commit is contained in:
@@ -1,66 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::f64::consts::{PI, SQRT_2};
|
|
||||||
use image::{GenericImageView, GrayImage};
|
|
||||||
|
|
||||||
fn dct(img: &image::DynamicImage) -> GrayImage {
|
|
||||||
//let mut dct_values: = [0.0; img.width() * img.height()];
|
|
||||||
let w = img.width();
|
|
||||||
let h = img.height();
|
|
||||||
let mut dct_img = GrayImage::new(w, h);
|
|
||||||
|
|
||||||
//let mut dct_values: Vec<f64> = Vec::new();
|
|
||||||
for u in 0..w {
|
|
||||||
for v in 0..h {
|
|
||||||
let w = w as f64;
|
|
||||||
let h = h as f64;
|
|
||||||
//let k = (v*img.width())+u;
|
|
||||||
let mut alpha = 0.25;
|
|
||||||
if u == 0 {
|
|
||||||
alpha = alpha / SQRT_2
|
|
||||||
}
|
|
||||||
if v == 0 {
|
|
||||||
alpha = alpha / SQRT_2
|
|
||||||
}
|
|
||||||
let v: f64 = v as f64;
|
|
||||||
let u: f64 = u as f64;
|
|
||||||
let mut sum: f64 = 0.0;
|
|
||||||
for (x, y, pix) in img.pixels() {
|
|
||||||
let x: f64 = x as f64;
|
|
||||||
let y: f64 = y as f64;
|
|
||||||
let pixel = pix[0] as f64;
|
|
||||||
// sum +=
|
|
||||||
// pixel *
|
|
||||||
// (x*u*PI/16.0).cos() *
|
|
||||||
// (y*v*PI/16.0).cos()
|
|
||||||
sum +=
|
|
||||||
pixel *
|
|
||||||
(
|
|
||||||
PI/w*
|
|
||||||
(x+0.5)*u
|
|
||||||
).cos()
|
|
||||||
*
|
|
||||||
(
|
|
||||||
PI/h*
|
|
||||||
(y+0.5)*v
|
|
||||||
).cos()
|
|
||||||
}
|
|
||||||
//dct_values[k] = alpha * sum;
|
|
||||||
//dct_values.push(alpha * sum);
|
|
||||||
print!("{sum}\t");
|
|
||||||
//sum = (sum + 200000.0)/784.0;
|
|
||||||
print!("{}\n", sum * alpha);
|
|
||||||
let pixel = (sum).abs() as u8;
|
|
||||||
println!("{u}, {v}:\t {sum}");
|
|
||||||
dct_img.put_pixel(u as u32, v as u32, image::Luma([pixel]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dct_img
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
let path = args[1].clone();
|
|
||||||
let img = image::open(&path).unwrap();
|
|
||||||
let dct = dct(&img);
|
|
||||||
dct.save("dct.png").unwrap();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use image_similarity::mutators::get_all_mutators;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Cfg {
|
||||||
|
/// Path to the image
|
||||||
|
path: String,
|
||||||
|
/// Try to output the images to terminal with viuer
|
||||||
|
#[arg(short, long, default_value_t=false)]
|
||||||
|
show_images: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let cfg = Cfg::parse();
|
||||||
|
|
||||||
|
let img = image::open(&cfg.path)
|
||||||
|
.expect("Unable to open file");
|
||||||
|
|
||||||
|
let mutators = get_all_mutators();
|
||||||
|
|
||||||
|
let conf = viuer::Config {
|
||||||
|
width: Some(16),
|
||||||
|
height: Some(8),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for mutator in mutators {
|
||||||
|
let mutated = mutator.mutate(&img);
|
||||||
|
let filename = "mut".to_string() + &mutator.tag() + &"jpg".to_string();
|
||||||
|
println!("Saving to {}", filename);
|
||||||
|
mutated.save(filename).expect("Saving image failed");
|
||||||
|
if cfg.show_images {
|
||||||
|
viuer::print(&mutated, &conf).expect("Image printing failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,7 +104,7 @@ impl DCT {
|
|||||||
|
|
||||||
impl Descriptor for DCT {
|
impl Descriptor for DCT {
|
||||||
fn info(&self) -> String {
|
fn info(&self) -> String {
|
||||||
"DCT".to_string()
|
"dct".to_string()
|
||||||
}
|
}
|
||||||
fn describe(&self, img: &image::DynamicImage) -> u64 {
|
fn describe(&self, img: &image::DynamicImage) -> u64 {
|
||||||
let resized = self.resize(img);
|
let resized = self.resize(img);
|
||||||
|
|||||||
+40
-28
@@ -1,6 +1,8 @@
|
|||||||
//! Descriptors that can be used to describe an image.
|
//! Descriptors that can be used to describe an image.
|
||||||
//! If two images are (almost) the same, their descriptions will be the same.
|
//! If two images are (almost) the same, their descriptions will be the same.
|
||||||
|
|
||||||
|
use image::GenericImageView;
|
||||||
|
|
||||||
pub trait Descriptor {
|
pub trait Descriptor {
|
||||||
/// Print the name of the descriptor,
|
/// Print the name of the descriptor,
|
||||||
fn info(&self) -> String;
|
fn info(&self) -> String;
|
||||||
@@ -36,34 +38,44 @@ pub struct DCT {
|
|||||||
}
|
}
|
||||||
pub mod dct;
|
pub mod dct;
|
||||||
|
|
||||||
// fn median64<T: Ord + Copy>(values: &[T]) -> T {
|
fn median64<T: Ord + Copy>(values: &[T]) -> T {
|
||||||
// let mut sorted_values = values.to_vec();
|
let mut sorted_values = values.to_vec();
|
||||||
// sorted_values.sort();
|
sorted_values.sort();
|
||||||
// let len = sorted_values.len();
|
let len = sorted_values.len();
|
||||||
// sorted_values[len/2]
|
sorted_values[len/2]
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
pub struct Median;
|
||||||
|
|
||||||
// pub struct Median;
|
impl Descriptor for Median {
|
||||||
|
fn info(&self) -> String {
|
||||||
|
"median".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
// impl Descriptor for Median {
|
fn describe(&self, img: &image::DynamicImage) -> u64 {
|
||||||
// fn describe(&self, img: image::DynamicImage) -> u64 {
|
let img = self.resize(img);
|
||||||
// let img = self.resize(img);
|
let mut values: [u8; 64] = [0; 64];
|
||||||
// let mut values: [u8; 64] = [0; 64];
|
let mut i: usize = 0;
|
||||||
// let mut i: usize = 0;
|
for (_, _, pix) in img.pixels() {
|
||||||
// for (_, _, pix) in img.pixels() {
|
values[i] = pix[0];
|
||||||
// values[i] = pix[0];
|
i = i+1;
|
||||||
// i = i+1;
|
}
|
||||||
// }
|
let median = median64(&values);
|
||||||
// let median = median64(&values);
|
let mut mask: u64 = 0;
|
||||||
// let mut mask: u64 = 0;
|
img.save("debug.png").unwrap();
|
||||||
// img.save("debug.png").unwrap();
|
for (_, _, pix) in img.pixels() {
|
||||||
// for (_, _, pix) in img.pixels() {
|
if pix[0] > median {
|
||||||
// if pix[0] > median {
|
mask += 1;
|
||||||
// mask += 1;
|
}
|
||||||
// }
|
mask = mask << 1;
|
||||||
// mask = mask << 1;
|
}
|
||||||
// }
|
mask
|
||||||
// mask
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
pub fn get_all_descriptors() -> Vec<Box<dyn Descriptor>> {
|
||||||
|
let mut descriptors: Vec<Box<dyn Descriptor>> = Vec::with_capacity(2);
|
||||||
|
descriptors.push(Box::new(DCT::new()));
|
||||||
|
descriptors.push(Box::new(Median));
|
||||||
|
descriptors
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@
|
|||||||
//! If two images are (almost) the same, their descriptions will be the same.
|
//! If two images are (almost) the same, their descriptions will be the same.
|
||||||
pub mod descriptors;
|
pub mod descriptors;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
pub mod mutators;
|
||||||
+44
-82
@@ -1,101 +1,63 @@
|
|||||||
use image_similarity::store::DescriptorStore;
|
use image_similarity::store::DescriptorStore;
|
||||||
use image_similarity::descriptors::{Descriptor, DCT};
|
use image_similarity::descriptors::get_all_descriptors;
|
||||||
use log::info;
|
use image_similarity::mutators::get_all_mutators;
|
||||||
|
use log::{error, info};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::thread;
|
use std::{fs, thread};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
struct Cfg {
|
struct Cfg {
|
||||||
/// Path that contains the input images. Will not traverse directories.
|
/// Path that contains the input images. Will not traverse directories.
|
||||||
path: PathBuf
|
path: PathBuf,
|
||||||
}
|
|
||||||
|
|
||||||
fn make_store<T: Descriptor>(descriptor: T, input_path: PathBuf, save_path: PathBuf) -> DescriptorStore<T> {
|
|
||||||
let mut store =
|
|
||||||
DescriptorStore::new(descriptor)
|
|
||||||
.with_file(save_path);
|
|
||||||
info!("{}", store);
|
|
||||||
store.insert_directory(input_path);
|
|
||||||
info!("{}", store);
|
|
||||||
store.save().expect("Error saving");
|
|
||||||
store
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StoreParams {
|
|
||||||
desc: DCT,
|
|
||||||
input_path: PathBuf,
|
|
||||||
save_path: PathBuf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let cfg = Cfg::parse();
|
let cfg = Cfg::parse();
|
||||||
|
let mutators = get_all_mutators();
|
||||||
|
//let descriptors = get_all_descriptors();
|
||||||
|
let mut stores: Vec<DescriptorStore> = Vec::new();
|
||||||
|
|
||||||
let mut threads = vec![];
|
for descriptor in get_all_descriptors() {
|
||||||
|
let store = DescriptorStore::new(descriptor);
|
||||||
let params = vec![
|
stores.push(store);
|
||||||
StoreParams {
|
|
||||||
desc: DCT::new().with_quality(50),
|
|
||||||
input_path: cfg.path.clone(),
|
|
||||||
save_path: "dct50.messagepack".into(),
|
|
||||||
},
|
|
||||||
// StoreParams {
|
|
||||||
// desc: DCT::new().with_quality(30),
|
|
||||||
// input_path: cfg.path.clone(),
|
|
||||||
// save_path: "dct30.messagepack".into(),
|
|
||||||
// },
|
|
||||||
// StoreParams {
|
|
||||||
// desc: DCT::new().with_quality(10),
|
|
||||||
// input_path: cfg.path.clone(),
|
|
||||||
// save_path: "dct10.messagepack".into(),
|
|
||||||
// },
|
|
||||||
];
|
|
||||||
|
|
||||||
// let dct_50 =
|
|
||||||
// DCT::new()
|
|
||||||
// .with_quality(50);
|
|
||||||
// let dct_30 =
|
|
||||||
// DCT::new()
|
|
||||||
// .with_quality(30);
|
|
||||||
// let dct_10 =
|
|
||||||
// DCT::new()
|
|
||||||
// .with_quality(10);
|
|
||||||
|
|
||||||
//let (tx, rx) = mpsc::channel::<DescriptorStore>();
|
|
||||||
|
|
||||||
for param in params {
|
|
||||||
threads.push(
|
|
||||||
thread::spawn(move || {
|
|
||||||
let store = make_store(param.desc, param.input_path, param.save_path);
|
|
||||||
store.print_most_dups();
|
|
||||||
//tx.send(store).unwrap();
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// let path_50 = cfg.path.clone();
|
for node in fs::read_dir(cfg.path).unwrap() {
|
||||||
// threads.push(
|
println!("{:?}", node);
|
||||||
// thread::spawn(move || {
|
let file = node.expect("Error walking directory");
|
||||||
// let store = make_store(dct_50, path_50, "dct50.messagepack".into());
|
let name = match file.file_name().into_string() {
|
||||||
// tx.send(store).expect("Thread send result error");
|
Ok(v) => v,
|
||||||
// })
|
Err(e) => {
|
||||||
// );
|
error!("Error reading {}: {:?}:", file.path().to_string_lossy(), e);
|
||||||
// let path_30 = cfg.path.clone();
|
continue
|
||||||
// threads.push(
|
}
|
||||||
// thread::spawn(move || {
|
};
|
||||||
// dct_30_store = make_store(dct_30, path_30, "dct30.messagepack".into());
|
let img = match image::open(file.path()) {
|
||||||
// })
|
Ok(v) => v,
|
||||||
// );
|
Err(e) => {
|
||||||
// let path_10 = cfg.path.clone();
|
error!("Failed to process {}: {}", name, e);
|
||||||
// threads.push(
|
continue
|
||||||
// thread::spawn(move || {
|
}
|
||||||
// dct_50_store = make_store(dct_10, path_10, "dct10.messagepack".into());
|
};
|
||||||
// })
|
|
||||||
// );
|
//Store the phashes of the base image
|
||||||
for thread in threads {
|
for store in &mut stores {
|
||||||
let _ = thread.join();
|
store.store(&img, name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
for mutator in &mutators {
|
||||||
|
let mutated = mutator.mutate(&img);
|
||||||
|
let mutated_name = "mut".to_string() + &mutator.tag() + &name.clone();
|
||||||
|
for store in &mut stores {
|
||||||
|
store.store(&mutated, mutated_name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for store in &stores {
|
||||||
|
println!("{}", store);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//! Mutators that can be used to mutate an image.
|
||||||
|
//! These mutated images can be used as a "near copy"
|
||||||
|
|
||||||
|
pub trait Mutator {
|
||||||
|
/// Name/description of the mutator
|
||||||
|
fn info(&self) -> String;
|
||||||
|
|
||||||
|
/// Short tag that represents the mutator.
|
||||||
|
/// To be used in the filename such that it can be recognized as mutated image
|
||||||
|
fn tag(&self) -> String {
|
||||||
|
"MUT".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mutated form of the input image
|
||||||
|
fn mutate(&self, img: &image::DynamicImage) -> image::DynamicImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Horizontal flip/mirror
|
||||||
|
pub struct Flip;
|
||||||
|
impl Mutator for Flip {
|
||||||
|
fn info(&self) -> String {
|
||||||
|
"Horizontal flip".to_string()
|
||||||
|
}
|
||||||
|
fn tag(&self) -> String {
|
||||||
|
".flip.".to_string()
|
||||||
|
}
|
||||||
|
fn mutate(&self, img: &image::DynamicImage) -> image::DynamicImage {
|
||||||
|
img.fliph()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slight hue-shift
|
||||||
|
pub struct Hue;
|
||||||
|
impl Mutator for Hue {
|
||||||
|
fn info(&self) -> String {
|
||||||
|
"Hue shift".to_string()
|
||||||
|
}
|
||||||
|
fn tag(&self) -> String {
|
||||||
|
".hue.".to_string()
|
||||||
|
}
|
||||||
|
fn mutate(&self, img: &image::DynamicImage) -> image::DynamicImage {
|
||||||
|
img.huerotate(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slight sharpen (unsharp mask)
|
||||||
|
pub struct Sharp;
|
||||||
|
impl Mutator for Sharp {
|
||||||
|
fn info(&self) -> String {
|
||||||
|
"Unsharp mask".to_string()
|
||||||
|
}
|
||||||
|
fn tag(&self) -> String {
|
||||||
|
".sharp.".to_string()
|
||||||
|
}
|
||||||
|
fn mutate(&self, img: &image::DynamicImage) -> image::DynamicImage {
|
||||||
|
img.unsharpen(1.2, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slight blur
|
||||||
|
pub struct Blur;
|
||||||
|
impl Mutator for Blur {
|
||||||
|
fn info(&self) -> String {
|
||||||
|
"Gaussian blur".to_string()
|
||||||
|
}
|
||||||
|
fn tag(&self) -> String {
|
||||||
|
".blur.".to_string()
|
||||||
|
}
|
||||||
|
fn mutate(&self, img: &image::DynamicImage) -> image::DynamicImage {
|
||||||
|
img.blur(1.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_mutators() -> Vec<Box<dyn Mutator>> {
|
||||||
|
let mut mutators: Vec<Box<dyn Mutator>> = Vec::with_capacity(4);
|
||||||
|
mutators.push(Box::new(Flip));
|
||||||
|
mutators.push(Box::new(Hue));
|
||||||
|
mutators.push(Box::new(Sharp));
|
||||||
|
mutators.push(Box::new(Blur));
|
||||||
|
mutators
|
||||||
|
}
|
||||||
+13
-5
@@ -6,6 +6,7 @@ use crate::descriptors::Descriptor;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use image::DynamicImage;
|
||||||
use log::{info, debug, error};
|
use log::{info, debug, error};
|
||||||
use bktree::*;
|
use bktree::*;
|
||||||
|
|
||||||
@@ -17,8 +18,8 @@ pub enum SaveError {
|
|||||||
|
|
||||||
/// Uses a hashmap to map descriptors to buckets of files.
|
/// Uses a hashmap to map descriptors to buckets of files.
|
||||||
/// Also keeps a BK-tree for quick distance ranking
|
/// Also keeps a BK-tree for quick distance ranking
|
||||||
pub struct DescriptorStore<T: Descriptor> {
|
pub struct DescriptorStore {
|
||||||
descriptor: T,
|
descriptor: Box<dyn Descriptor>,
|
||||||
/// Main hashmap that maps descriptors to buckets of filenames
|
/// Main hashmap that maps descriptors to buckets of filenames
|
||||||
map: HashMap<u64, Vec<String>>,
|
map: HashMap<u64, Vec<String>>,
|
||||||
|
|
||||||
@@ -32,9 +33,9 @@ pub struct DescriptorStore<T: Descriptor> {
|
|||||||
seen: Option<HashSet<String>>,
|
seen: Option<HashSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Descriptor> DescriptorStore<T> {
|
impl DescriptorStore {
|
||||||
/// Makes a new empty DescriptorStore with default settings
|
/// Makes a new empty DescriptorStore with default settings
|
||||||
pub fn new(descriptor: T) -> Self {
|
pub fn new(descriptor: Box<dyn Descriptor>) -> Self {
|
||||||
let map: HashMap<u64, Vec<String>> = HashMap::new();
|
let map: HashMap<u64, Vec<String>> = HashMap::new();
|
||||||
//let seen: HashSet<String> = HashSet::new();
|
//let seen: HashSet<String> = HashSet::new();
|
||||||
//let seen = None;
|
//let seen = None;
|
||||||
@@ -154,6 +155,13 @@ impl<T: Descriptor> DescriptorStore<T> {
|
|||||||
self.seen = None;
|
self.seen = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn store(&mut self, img: &DynamicImage, name: String) {
|
||||||
|
if !self.has_value(&name) {
|
||||||
|
let phash = self.descriptor.describe(&img);
|
||||||
|
self.insert(&phash, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Nearest neighbours
|
/// Nearest neighbours
|
||||||
pub fn nn(&self, from: u64, max_distance: isize) -> Vec<(&u64, isize)> {
|
pub fn nn(&self, from: u64, max_distance: isize) -> Vec<(&u64, isize)> {
|
||||||
let mut neighbours = self.bktree.find(from, max_distance);
|
let mut neighbours = self.bktree.find(from, max_distance);
|
||||||
@@ -224,7 +232,7 @@ impl<T: Descriptor> DescriptorStore<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Descriptor> fmt::Display for DescriptorStore<T> {
|
impl fmt::Display for DescriptorStore {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
for (key, bucket) in self.map.iter() {
|
for (key, bucket) in self.map.iter() {
|
||||||
|
|||||||
Reference in New Issue
Block a user