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
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 errorThe 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 errorHow 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 errorTwo 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 errorThe 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.qweOr 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.pngThen, in your program, all of it or only what you call:
use image_hash.qweruse image_hash, image_hash_distance from image_hash // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
image_hasher = "3"image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }Example
// 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
//! 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())}Related
exif.qwer · 1.0.0A photo's EXIF: camera and lens, when it was taken, where (GPS), exposure and orientation, from JPEG, HEIC, PNG and WebP.7 views · 5 downloadsEXIF strip exif_strip.qwer · 1.0.0Photos made safe to publish: GPS, camera and other EXIF, XMP, IPTC and comments taken out of JPEG, PNG and WebP, without re-encoding.6 views · 7 downloadsImage edits image_ops.qwer · 1.0.0Crop, rotate, flip, grayscale, blur, and phone photos turned upright by their EXIF orientation, saved as JPEG, PNG, WebP or GIF.7 views · 7 downloadsOCR ocr.qwer · 1.0.0Text read from pictures in pure Rust: photos and scans of documents, receipts, labels and screenshots to plain text, row by row or line by line with boxes, parts of the page read by range (pixels or percentages), several form fields at once, only the characters you allow, and phone photos turned upright first.13 views · 9 downloadsQR and barcode reader qr_read.qwer · 1.0.0QR codes and barcodes read from pictures: QR, Data Matrix, PDF417, EAN-13, UPC-A, Code 128 and more, for tickets, parcels and products.8 views · 7 downloadsWatermark watermark.qwer · 1.0.0A logo laid over photos at a corner or the centre, or tiled across, with the opacity you choose, for publishing pictures with your mark.8 views · 8 downloadsThread 0 posts
On Image hash: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.