This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::f64::consts::{PI, SQRT_2};
|
use std::f64::consts::{PI, SQRT_2};
|
||||||
use image::{save_buffer, GenericImageView, GrayImage};
|
use image::{GenericImageView, GrayImage};
|
||||||
|
|
||||||
fn dct(img: &image::DynamicImage) -> GrayImage {
|
fn dct(img: &image::DynamicImage) -> GrayImage {
|
||||||
//let mut dct_values: = [0.0; img.width() * img.height()];
|
//let mut dct_values: = [0.0; img.width() * img.height()];
|
||||||
|
|||||||
+19
-29
@@ -1,50 +1,40 @@
|
|||||||
//use image_similarity::descriptors::dct::DCT;
|
//use image_similarity::descriptors::dct::DCT;
|
||||||
use image_similarity::descriptors::{DCT, Descriptor};
|
use image_similarity::descriptors::{DCT, Descriptor};
|
||||||
use image_similarity::store::DescriptorStore;
|
use image_similarity::store::DescriptorStore;
|
||||||
use std::env;
|
use log::info;
|
||||||
use log::{info, debug, error};
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
struct Cfg {
|
struct Cfg {
|
||||||
path: String
|
/// Path to the image
|
||||||
}
|
path: String,
|
||||||
|
/// Try to output the images to terminal with viuer
|
||||||
impl Cfg{
|
#[arg(short, long, default_value_t=false)]
|
||||||
fn load(args: &[String]) -> Result<Cfg, &'static str> {
|
show_images: bool,
|
||||||
if args.len() < 2 {
|
distance: isize,
|
||||||
return Err("Filename argument required");
|
|
||||||
}
|
|
||||||
let path = args[1].clone();
|
|
||||||
Ok(Cfg { path })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
//Init all descriptors:
|
|
||||||
let desc = DCT::new().with_quality(50);
|
let desc = DCT::new().with_quality(50);
|
||||||
|
|
||||||
let args: Vec<String> = env::args().collect();
|
let cfg = Cfg::parse();
|
||||||
|
|
||||||
let cfg = Cfg::load(&args)
|
|
||||||
.expect("Error loading config");
|
|
||||||
|
|
||||||
let img = image::open(cfg.path)
|
let img = image::open(cfg.path)
|
||||||
.expect("Unable to open file");
|
.expect("Unable to open file");
|
||||||
println!("Iamge");
|
// let conf = viuer::Config {
|
||||||
let conf = viuer::Config {
|
// width: Some(16),
|
||||||
width: Some(16),
|
// height: Some(8),
|
||||||
height: Some(8),
|
// ..Default::default()
|
||||||
..Default::default()
|
// };
|
||||||
};
|
// viuer::print(&img, &conf).expect("Image printing failed.");
|
||||||
viuer::print(&img, &conf).expect("Image printing failed.");
|
|
||||||
println!("Iamge");
|
|
||||||
let store =
|
let store =
|
||||||
DescriptorStore::new()
|
DescriptorStore::new()
|
||||||
.with_file("store.messagepack");
|
.with_file("dct50.messagepack");
|
||||||
//info!("{}", store);
|
|
||||||
let phash: u64 = desc.describe(&img);
|
let phash: u64 = desc.describe(&img);
|
||||||
info!("Phash integer:\n{phash}");
|
info!("Phash integer:\n{phash}");
|
||||||
info!("Phash binary:\n{phash:064b}");
|
info!("Phash binary:\n{phash:064b}");
|
||||||
store.knn(phash, 10);
|
store.print_nn(phash, cfg.distance, cfg.show_images);
|
||||||
}
|
}
|
||||||
@@ -174,8 +174,8 @@ impl Descriptor for DCT {
|
|||||||
let signum = dct_values[i].signum();
|
let signum = dct_values[i].signum();
|
||||||
// Only multiply sign if dct-coefficient contains a horizontal component.
|
// Only multiply sign if dct-coefficient contains a horizontal component.
|
||||||
if
|
if
|
||||||
//i % 8 != 0 && sign_mult * signum < 0.0
|
i % 8 != 0 && sign_mult * signum < 0.0
|
||||||
// ||
|
||
|
||||||
signum < 0.0
|
signum < 0.0
|
||||||
{
|
{
|
||||||
sign_mask += 1;
|
sign_mask += 1;
|
||||||
|
|||||||
+64
-22
@@ -12,7 +12,7 @@ struct Cfg {
|
|||||||
path: PathBuf
|
path: PathBuf
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_store<T: Descriptor>(descriptor: T, input_path: PathBuf, save_path: PathBuf) -> () {
|
fn make_store<T: Descriptor>(descriptor: T, input_path: PathBuf, save_path: PathBuf) -> DescriptorStore {
|
||||||
let mut store =
|
let mut store =
|
||||||
DescriptorStore::new()
|
DescriptorStore::new()
|
||||||
.with_file(save_path);
|
.with_file(save_path);
|
||||||
@@ -20,6 +20,13 @@ fn make_store<T: Descriptor>(descriptor: T, input_path: PathBuf, save_path: Path
|
|||||||
store.insert_directory(input_path, descriptor);
|
store.insert_directory(input_path, descriptor);
|
||||||
info!("{}", store);
|
info!("{}", store);
|
||||||
store.save().expect("Error saving");
|
store.save().expect("Error saving");
|
||||||
|
store
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StoreParams {
|
||||||
|
desc: DCT,
|
||||||
|
input_path: PathBuf,
|
||||||
|
save_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -28,32 +35,67 @@ fn main() {
|
|||||||
|
|
||||||
let mut threads = vec![];
|
let mut threads = vec![];
|
||||||
|
|
||||||
let dct_50 =
|
let params = vec![
|
||||||
DCT::new()
|
StoreParams {
|
||||||
.with_quality(50);
|
desc: DCT::new().with_quality(50),
|
||||||
let dct_30 =
|
input_path: cfg.path.clone(),
|
||||||
DCT::new()
|
save_path: "dct50.messagepack".into(),
|
||||||
.with_quality(30);
|
},
|
||||||
let dct_10 =
|
// StoreParams {
|
||||||
DCT::new()
|
// desc: DCT::new().with_quality(30),
|
||||||
.with_quality(10);
|
// 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(
|
threads.push(
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
make_store(dct_50, "data".into(), "dct50.messagepack".into());
|
let store = make_store(param.desc, param.input_path, param.save_path);
|
||||||
|
store.print_most_dups();
|
||||||
|
//tx.send(store).unwrap();
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
threads.push(
|
}
|
||||||
thread::spawn(move || {
|
|
||||||
make_store(dct_30, "data".into(), "dct30.messagepack".into());
|
// let path_50 = cfg.path.clone();
|
||||||
})
|
// threads.push(
|
||||||
);
|
// thread::spawn(move || {
|
||||||
threads.push(
|
// let store = make_store(dct_50, path_50, "dct50.messagepack".into());
|
||||||
thread::spawn(move || {
|
// tx.send(store).expect("Thread send result error");
|
||||||
make_store(dct_10, "data".into(), "dct10.messagepack".into());
|
// })
|
||||||
})
|
// );
|
||||||
);
|
// let path_30 = cfg.path.clone();
|
||||||
|
// threads.push(
|
||||||
|
// thread::spawn(move || {
|
||||||
|
// dct_30_store = make_store(dct_30, path_30, "dct30.messagepack".into());
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// let path_10 = cfg.path.clone();
|
||||||
|
// threads.push(
|
||||||
|
// thread::spawn(move || {
|
||||||
|
// dct_50_store = make_store(dct_10, path_10, "dct10.messagepack".into());
|
||||||
|
// })
|
||||||
|
// );
|
||||||
for thread in threads {
|
for thread in threads {
|
||||||
let _ = thread.join();
|
let _ = thread.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-28
@@ -1,7 +1,7 @@
|
|||||||
//! Persistent store of all calculated image descriptions.
|
//! Persistent store of all calculated image descriptions.
|
||||||
//! Can be queried to find identical or similar images.
|
//! Can be queried to find identical or similar images.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use crate::descriptors::Descriptor;
|
use crate::descriptors::Descriptor;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -15,20 +15,33 @@ pub enum SaveError {
|
|||||||
File,
|
File,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Uses a hashmap to map descriptors to buckets of files.
|
||||||
|
/// Also keeps a BK-tree for quick distance ranking
|
||||||
pub struct DescriptorStore {
|
pub struct DescriptorStore {
|
||||||
map: HashMap<u64, String>,
|
/// Main hashmap that maps descriptors to buckets of filenames
|
||||||
|
map: HashMap<u64, Vec<String>>,
|
||||||
|
|
||||||
|
/// Place to load and store the map on file
|
||||||
save_location: std::path::PathBuf,
|
save_location: std::path::PathBuf,
|
||||||
|
|
||||||
|
/// BK-tree for fast nearest neighbour
|
||||||
bktree: BkTree<u64>,
|
bktree: BkTree<u64>,
|
||||||
|
|
||||||
|
/// Cache for seen files to skip on initial load
|
||||||
|
seen: Option<HashSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DescriptorStore {
|
impl DescriptorStore {
|
||||||
/// Makes a new empty DescriptorStore with default settings
|
/// Makes a new empty DescriptorStore with default settings
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let map: HashMap<u64, String> = HashMap::new();
|
let map: HashMap<u64, Vec<String>> = HashMap::new();
|
||||||
|
//let seen: HashSet<String> = HashSet::new();
|
||||||
|
//let seen = None;
|
||||||
DescriptorStore {
|
DescriptorStore {
|
||||||
map,
|
map,
|
||||||
save_location: std::path::PathBuf::from("store.messagepack"),
|
save_location: std::path::PathBuf::from("store.messagepack"),
|
||||||
bktree: BkTree::new(hamming_distance)
|
bktree: BkTree::new(hamming_distance),
|
||||||
|
seen: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,12 +50,21 @@ impl DescriptorStore {
|
|||||||
self.save_location = std::path::PathBuf::from(path.as_ref());
|
self.save_location = std::path::PathBuf::from(path.as_ref());
|
||||||
let map_file = fs::read(&self.save_location);
|
let map_file = fs::read(&self.save_location);
|
||||||
match map_file {
|
match map_file {
|
||||||
Ok(f) => self.map = rmp_serde::from_slice(&f).unwrap(),
|
Ok(f) => {
|
||||||
Err(_) => info!("{} not found, starting from empty store.", self.save_location.display()),
|
self.map = rmp_serde::from_slice(&f).unwrap();
|
||||||
};
|
|
||||||
for i in self.map.keys() {
|
for i in self.map.keys() {
|
||||||
self.bktree.insert(*i);
|
self.bktree.insert(*i);
|
||||||
}
|
}
|
||||||
|
let mut seen: HashSet<String> = HashSet::new();
|
||||||
|
for bucket in self.map.values() {
|
||||||
|
for element in bucket {
|
||||||
|
seen.insert(element.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.seen = Some(seen);
|
||||||
|
},
|
||||||
|
Err(_) => info!("{} not found, starting from empty store.", self.save_location.display()),
|
||||||
|
};
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +84,38 @@ impl DescriptorStore {
|
|||||||
self.map.contains_key(&key)
|
self.map.contains_key(&key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true iff the store already contains the value in some bucket.
|
||||||
|
/// Will use a hashmap cache if available
|
||||||
|
pub fn has_value(&self, value: &String) -> bool {
|
||||||
|
match &self.seen {
|
||||||
|
Some(set) => {
|
||||||
|
set.contains(value)
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
for bucket in self.map.values() {
|
||||||
|
if bucket.contains(&value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Inserts a single value into the store
|
/// Inserts a single value into the store
|
||||||
pub fn insert(&mut self, key: u64, value: String) {
|
pub fn insert(&mut self, key: &u64, value: String) {
|
||||||
self.map.insert(key, value);
|
let bucket = match self.map.get(&key) {
|
||||||
self.bktree.insert(key);
|
Some(b) => {
|
||||||
|
let mut n = b.clone();
|
||||||
|
if !n.contains(&value) {
|
||||||
|
n.push(value);
|
||||||
|
}
|
||||||
|
n
|
||||||
|
},
|
||||||
|
None => vec![value],
|
||||||
|
};
|
||||||
|
self.map.insert(*key, bucket);
|
||||||
|
self.bktree.insert(*key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates all descriptions with a given descriptor
|
/// Calculates all descriptions with a given descriptor
|
||||||
@@ -79,7 +129,7 @@ impl DescriptorStore {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if true { // !self.contains(name.to_string()) {
|
if !self.has_value(&name) { // !self.contains(name.to_string()) {
|
||||||
info!("Processing {}", name);
|
info!("Processing {}", name);
|
||||||
let img = match image::open(file.path()) {
|
let img = match image::open(file.path()) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
@@ -92,26 +142,36 @@ impl DescriptorStore {
|
|||||||
if self.contains(phash) {
|
if self.contains(phash) {
|
||||||
println!("{} duplicate of {:?}", name, self.map.get(&phash));
|
println!("{} duplicate of {:?}", name, self.map.get(&phash));
|
||||||
}
|
}
|
||||||
self.map.insert(phash, name);
|
self.insert(&phash, name);
|
||||||
} else {
|
} else {
|
||||||
debug!("{} already known, skipping.", name);
|
debug!("{} already known, skipping.", name);
|
||||||
}
|
}
|
||||||
self.save().expect("error");
|
self.save().expect("error");
|
||||||
}
|
}
|
||||||
|
// We have done our bulk loading, so we can unset our hashset cache:
|
||||||
|
self.seen = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// K-nearest neighbors
|
/// Nearest neighbours
|
||||||
pub fn knn(&self, from: u64, k: isize) {
|
pub fn nn(&self, from: u64, max_distance: isize) -> Vec<(&u64, isize)> {
|
||||||
println!("{from:b}");
|
let mut neighbours = self.bktree.find(from, max_distance);
|
||||||
|
neighbours.sort_by(|a, b| a.1.cmp(&b.1));
|
||||||
|
neighbours
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_nn(&self, from: u64, max_distance: isize, show_images: bool) {
|
||||||
|
//println!("{from:064b}");
|
||||||
|
let neighbours = self.nn(from, max_distance);
|
||||||
|
let (term_width, _) = viuer::terminal_size();
|
||||||
let mut x = 0;
|
let mut x = 0;
|
||||||
let mut y = 8;
|
let mut y = 8;
|
||||||
let (term_width, _) = viuer::terminal_size();
|
for (element, distance) in neighbours {
|
||||||
println!("\t\t\t\t\tTERMWIDTH {term_width}");
|
let elements = self.map.get(element);
|
||||||
for (element, distance) in self.bktree.find(from, k) {
|
match elements {
|
||||||
let name = self.map.get(element);
|
Some(paths) => {
|
||||||
match name {
|
for path in paths {
|
||||||
Some(n) => {
|
println!("{element:064b}: {:?} (distance: {distance})", path);
|
||||||
println!("{element:b}: {n} (distance: {distance})");
|
if show_images {
|
||||||
if x+16 >= term_width {
|
if x+16 >= term_width {
|
||||||
x = 0;
|
x = 0;
|
||||||
y += 8;
|
y += 8;
|
||||||
@@ -119,27 +179,60 @@ impl DescriptorStore {
|
|||||||
let conf = viuer::Config {
|
let conf = viuer::Config {
|
||||||
width: Some(16),
|
width: Some(16),
|
||||||
height: Some(8),
|
height: Some(8),
|
||||||
x: x,
|
x,
|
||||||
y: y,
|
y,
|
||||||
|
use_kitty: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
x += 16;
|
x += 16;
|
||||||
let path = "data/".to_string() + &n;
|
let path = "data/".to_string() + path;
|
||||||
let img = image::open(&path).unwrap();
|
let img = image::open(&path).unwrap();
|
||||||
|
let img = img.grayscale().thumbnail_exact(8, 8);
|
||||||
viuer::print(&img, &conf).expect("Image printing failed.");
|
viuer::print(&img, &conf).expect("Image printing failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
None => ()
|
None => ()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn print_most_dups(&self) {
|
||||||
|
let mut most = 0;
|
||||||
|
for bucket in self.map.values() {
|
||||||
|
let len = bucket.len();
|
||||||
|
if len > most {
|
||||||
|
most = len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
for (key, bucket) in self.map.iter() {
|
||||||
|
if bucket.len() == most {
|
||||||
|
let key_entry = format!("{key:064b}:\n\t\t");
|
||||||
|
output.push_str(&key_entry);
|
||||||
|
for value in bucket {
|
||||||
|
let value_entry = format!("{value}\n");
|
||||||
|
output.push_str(&value_entry);
|
||||||
|
}
|
||||||
|
output.push_str("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print!("DescriptorStore:\n{}Total: {}", output, self.map.len())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DescriptorStore {
|
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, val) in self.map.iter() {
|
for (key, bucket) in self.map.iter() {
|
||||||
let entry = format!("{key:064b}: {val}\t {val:020}\n");
|
let key_entry = format!("{key:064b}:\n");
|
||||||
output.push_str(&entry);
|
output.push_str(&key_entry);
|
||||||
|
for value in bucket {
|
||||||
|
let value_entry = format!("\t{value}\n");
|
||||||
|
output.push_str(&value_entry);
|
||||||
|
}
|
||||||
|
output.push_str("\n");
|
||||||
}
|
}
|
||||||
write!(f, "DescriptorStore:\n{}Total: {}", output, self.map.len())
|
write!(f, "DescriptorStore:\n{}Total: {}", output, self.map.len())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user