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}