image-match 0.2.4

Derives an image signature to be used in quick image comparisons
Documentation
#![cfg(feature = "img")]

use std::collections::HashMap;
use std::ffi::OsString;
use std::fs;
use std::path::Path;

use image_match::cosine_similarity;
use image_match::image::get_file_signature;

#[test]
fn check_match_percentages() {
    let orig = calc_sigs_for_pic_dir_files("original");
    let cropped = calc_sigs_for_pic_dir_files("cropped");
    let grown = calc_sigs_for_pic_dir_files("grown");
    let shrunk = calc_sigs_for_pic_dir_files("shrunk");

    evaluate_altered("Cropped", &orig, &cropped);
    evaluate_altered("Grown", &orig, &grown);
    evaluate_altered("Shrunk", &orig, &shrunk);
    evaluate_non_matching(&orig);
}

fn calc_sigs_for_pic_dir_files(dir: &str) -> HashMap<String, Vec<i8>> {
    println!("Calculating signatures for {}", dir);
    let pics_root = Path::new("./tests/pics").join(Path::new(dir));
    let names: Vec<OsString> = fs::read_dir(pics_root.clone()).unwrap()
        .map(|f| f.unwrap().file_name())
        .collect();
    let mut files = HashMap::with_capacity(names.len());
    for name in names {
        let path = pics_root.join(Path::new(&name));
        let signature = get_file_signature(path).unwrap();
        files.insert(name.into_string().unwrap(), signature);
    }

    files
}

fn evaluate_altered(name: &str, orig: &HashMap<String, Vec<i8>>, altered: &HashMap<String, Vec<i8>>) {
    let mut cosines = Vec::with_capacity(orig.len());
    for (file, signature) in orig {
        let altered_sig = altered.get(file).unwrap();
        cosines.push(cosine_similarity(signature, altered_sig));
    }

    print_stats(name, cosines);
}

fn evaluate_non_matching(orig: &HashMap<String, Vec<i8>>) {
    let mut non_matching = Vec::with_capacity(orig.len() * orig.len());
    for (name, signature) in orig {
        orig.iter().filter(|(n, _)| n != &name)
            .map(|(_, other_sig)| cosine_similarity(signature, other_sig))
            .for_each(|similarity| non_matching.push(similarity));
    }

    print_stats("Non-Matching", non_matching);
}

fn print_stats(name: &str, mut cosines: Vec<f64>) {
    cosines.sort_by(|a, b| a.partial_cmp(b).unwrap());

    let avg = cosines.iter().sum::<f64>() / cosines.len() as f64;

    println!("{}:", name);
    println!("Min\tMean\tMax\t30th\t50th\t75th\t90th\t95th\t99th");
    println!("{:.3}\t{:.3}\t{:.3}\t{:.3}\t{:.3}\t{:.3}\t{:.3}\t{:.3}\t{:.3}\n",
             cosines.first().unwrap(),
             avg,
             cosines.last().unwrap(),
             prcnt(&cosines, 0.30),
             prcnt(&cosines, 0.50),
             prcnt(&cosines, 0.75),
             prcnt(&cosines, 0.90),
             prcnt(&cosines, 0.95),
             prcnt(&cosines, 0.99),
    );
}

fn prcnt(cosines: &Vec<f64>, percentile: f64) -> f64 {
    let idx = (percentile * cosines.len() as f64).floor() as usize;
    *cosines.get(idx).unwrap()
}