code wiki / (root) / nx_phash.nx

nx_phash.nx source

↩ module page · 102 lines · 3562 B

1// nx_phash.nx -- perceptual image hashing + reverse-image (bits-up). 2// 3// module: nishi-core.search.phash 4// depends: fx.nx, syscalls.nx, nx_simhash.nx (reuse 64-bit Hamming) 5// capability: CORE_COMPUTE 6// wired_status: FULLY_WIRED 7// 8// genealogy_id: krawetz_2011_looks_like_it (aHash/dHash) + zauner_2010_phash 9// 10// WHY (operator: Yandex-style "find this image across our crawl", done our way): 11// a deterministic visual fingerprint so the crawler can dedup/cluster images 12// and answer reverse-image queries. dHash is the research-backed best-ROI 13// choice -- it encodes the RELATIVE order of adjacent pixels, so it is 14// invariant to uniform brightness/contrast and robust to recompression/resize, 15// yet needs no DCT (pure integer downscale + compares). Distance reuses the 16// SimHash Hamming kernel; "find this image" = nearest fingerprint. Pixels come 17// from the existing JPEG decoder + nx_image; the input here is a grayscale 18// buffer so the hasher stays format-agnostic. 19 20import "fx.nx" 21import "syscalls.nx" 22import "nx_simhash.nx" 23 24// nearest-neighbour downscale of a grayscale (w x h, row-major u8) image into 25// out (dw x dh). 26func nx_phash_downscale(gray: *u8, w: i64, h: i64, out: *u8, dw: i64, dh: i64) -> i64 { 27 var oy: i64 = 0 28 while oy < dh { 29 let sy: i64 = (oy * h) / dh 30 var ox: i64 = 0 31 while ox < dw { 32 let sx: i64 = (ox * w) / dw 33 out[oy * dw + ox] = gray[sy * w + sx] 34 ox = ox + 1 35 } 36 oy = oy + 1 37 } 38 return 0 39} 40 41// dHash (Krawetz 2011): downscale to 9x8, set each of 64 bits by comparing 42// horizontally-adjacent pixels. Invariant to uniform brightness because it 43// encodes relative order, not absolute value. Returns a 64-bit fingerprint. 44func nx_phash_dhash(gray: *u8, w: i64, h: i64) -> i64 { 45 let s: *u8 = sys_mmap(9 * 8) 46 nx_phash_downscale(gray, w, h, s, 9, 8) 47 var fp: i64 = 0 48 var bit: i64 = 0 49 var y: i64 = 0 50 while y < 8 { 51 var x: i64 = 0 52 while x < 8 { 53 let left: i64 = s[y * 9 + x] as i64 54 let right: i64 = s[y * 9 + x + 1] as i64 55 if left > right { fp = fp | (1 << bit) } 56 bit = bit + 1 57 x = x + 1 58 } 59 y = y + 1 60 } 61 return fp 62} 63 64// aHash (average hash): downscale to 8x8, bit = pixel > mean. Simpler, a touch 65// less robust than dHash; useful as a second signal. 66func nx_phash_ahash(gray: *u8, w: i64, h: i64) -> i64 { 67 let s: *u8 = sys_mmap(8 * 8) 68 nx_phash_downscale(gray, w, h, s, 8, 8) 69 var sum: i64 = 0 70 var i: i64 = 0 71 while i < 64 { sum = sum + (s[i] as i64); i = i + 1 } 72 let mean: i64 = sum / 64 73 var fp: i64 = 0 74 i = 0 75 while i < 64 { 76 if (s[i] as i64) > mean { fp = fp | (1 << i) } 77 i = i + 1 78 } 79 return fp 80} 81 82// Q16.16 visual similarity from a perceptual-hash Hamming distance. 83func nx_phash_similarity_q16(a: i64, b: i64) -> i64 { 84 return fx_from_frac(64 - nx_simhash_hamming(a, b), 64) 85} 86 87// reverse-image: index of the closest fingerprint in fps[0..n) within 88// `threshold` Hamming, or -1 if none qualifies. Writes the best Hamming to 89// out_ham[0]. This is "find this image across our crawl". 90func nx_phash_nearest(query: i64, fps: *i64, n: i64, threshold: i64, out_ham: *i64) -> i64 { 91 var best: i64 = 0 - 1 92 var bh: i64 = 65 93 var i: i64 = 0 94 while i < n { 95 let h: i64 = nx_simhash_hamming(query, fps[i]) 96 if h < bh { bh = h; best = i } 97 i = i + 1 98 } 99 out_ham[0] = bh 100 if bh <= threshold { return best } 101 return 0 - 1 102}