Media

Image hash

Perceptual hashes of pictures and how far apart two are, to find near-duplicates among uploads or a photo library.

Version 1.0.0 · 4 functions · 2 crates

#images#duplicates#uploads#search

Functions

What use image_hash.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use image_hash, image_hash_distance from image_hash (Quillo 0.6).

image_hash(path: str) → stror an error

The picture's perceptual hash, as 16 hex characters (64 bits) to keep in a database: pictures that look alike have hashes that differ in few bits.

image_hash_distance(a: str, b: str) → intor an error

How many bits two hashes differ in: 0 for the same picture, up to about 10 for a resized or recompressed copy, 20 and more for different pictures.

image_hash_compare(a: str, b: str) → anyor an error

Two pictures compared: { distance, similar }, similar true when the distance is 10 or less.

image_hash_nearest(hash: str, candidates: list, max_distance: int) → listor an error

The candidates (each { id, hash }, as kept from image_hash) within max_distance of hash, nearest first, as { id, distance }: "have we seen this photo before?"

Get it

$ quillo install image_hash               # image_hash.qwer into this folder$ quillo install image_hash --example     # and its example, image_hash.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/image_hash.qwer$ curl -O https://quillolang.org/plugins/files/image_hash.qwe$ curl -O https://quillolang.org/plugins/files/image_hash_sample.jpg$ curl -O https://quillolang.org/plugins/files/image_hash_sample.png

Then, in your program, all of it or only what you call:

main.qwe
use image_hash.qweruse image_hash, image_hash_distance from image_hash      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
image_hasher = "3"image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }

Example

image_hash.qwe
// Duplicate photo detection: a smaller, recompressed copy is found; a different picture is not.////   quillo run image_hash.qwe      then open http://localhost:8080/duplicatesuse "image_hash.qwer"route GET /duplicates {    image.convert("image_hash_sample.jpg", "data/hash/copy.jpg", { width: 160, format: "jpeg", quality: 50 })?    {        copy: image_hash_compare("image_hash_sample.jpg", "data/hash/copy.jpg")?,        other: image_hash_compare("image_hash_sample.jpg", "image_hash_sample.png")?,    }}// The library keeps each photo's hash; a new upload is looked up among them.route GET /seen {    image.convert("image_hash_sample.jpg", "data/hash/copy.jpg", { width: 160, format: "jpeg", quality: 50 })?    let library = [        { id: 1, hash: image_hash("image_hash_sample.png")? },        { id: 2, hash: image_hash("image_hash_sample.jpg")? },    ]    { hash: image_hash("data/hash/copy.jpg")?, matches: image_hash_nearest(image_hash("data/hash/copy.jpg")?, library, 10)? }}
$ quillo run image_hash.qwe$ curl 'localhost:8080/duplicates'$ curl 'localhost:8080/seen'

Source

image_hash.qwer
//! Perceptual hashes of pictures, and how far apart two are: to find near-duplicates (resized, recompressed, slightly edited) among uploads or a photo library.//! version: 1.0.0//!//! [dependencies]//! image_hasher = "3"//! image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }use serde_json::{json, Value};const SIMILAR: u32 = 10;fn open(path: &str) -> Result<image::DynamicImage, String> {    let mut r = image::ImageReader::open(path).map_err(|e| format!("{}: {}", path, e))?.with_guessed_format().map_err(|e| format!("{}: {}", path, e))?;    let mut l = image::Limits::default();    l.max_image_width = Some(12_000);    l.max_image_height = Some(12_000);    l.max_alloc = Some(512 * 1024 * 1024);    r.limits(l);    r.decode().map_err(|e| format!("{}: {}", path, e))}fn hash_bytes(path: &str) -> Result<Vec<u8>, String> {    let img = open(path)?;    let h = image_hasher::HasherConfig::new().hash_size(8, 8).hash_alg(image_hasher::HashAlg::Gradient).to_hasher();    Ok(h.hash_image(&img).as_bytes().to_vec())}fn hex(b: &[u8]) -> String {    b.iter().map(|x| format!("{:02x}", x)).collect()}fn unhex(s: &str) -> Result<Vec<u8>, String> {    let s = s.trim();    if s.len() % 2 != 0 || !s.chars().all(|c| c.is_ascii_hexdigit()) {        return Err(format!("`{}` is not a hash from image_hash", s));    }    (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string())).collect()}fn distance(a: &[u8], b: &[u8]) -> Result<u32, String> {    if a.len() != b.len() {        return Err("the hashes are of different sizes".into());    }    Ok(a.iter().zip(b).map(|(x, y)| (x ^ y).count_ones()).sum())}/// The picture's perceptual hash, as 16 hex characters (64 bits) to keep in a database: pictures that look alike have hashes that differ in few bits.pub fn image_hash(path: &str) -> Result<String, String> {    Ok(hex(&hash_bytes(path)?))}/// How many bits two hashes differ in: 0 for the same picture, up to about 10 for a resized or recompressed copy, 20 and more for different pictures.pub fn image_hash_distance(a: &str, b: &str) -> Result<i64, String> {    Ok(distance(&unhex(a)?, &unhex(b)?)? as i64)}/// Two pictures compared: `{ distance, similar }`, `similar` true when the distance is 10 or less.pub fn image_hash_compare(a: &str, b: &str) -> Result<Value, String> {    let d = distance(&hash_bytes(a)?, &hash_bytes(b)?)?;    Ok(json!({ "distance": d, "similar": d <= SIMILAR }))}/// The `candidates` (each `{ id, hash }`, as kept from `image_hash`) within `max_distance` of `hash`, nearest first, as `{ id, distance }`: "have we seen this photo before?"pub fn image_hash_nearest(hash: &str, candidates: Vec<serde_json::Value>, max_distance: i64) -> Result<Vec<Value>, String> {    let h = unhex(hash)?;    let mut out: Vec<(u32, Value)> = vec![];    for c in &candidates {        let Some(ch) = c.get("hash").and_then(Value::as_str) else { continue };        let Ok(cb) = unhex(ch) else { continue };        if let Ok(d) = distance(&h, &cb) {            if d as i64 <= max_distance {                out.push((d, json!({ "id": c.get("id").cloned().unwrap_or(Value::Null), "distance": d })));            }        }    }    out.sort_by_key(|(d, _)| *d);    Ok(out.into_iter().map(|(_, v)| v).collect())}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On Image hash: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.