nx_imgbench.nx source
↩ module page · 287 lines · 14852 B
1// nx_imgbench.nx -- THE REVERSE-IMAGE RULER. Answers, by measurement rather than opinion, the only
2// question that matters on the road to TinEye/Google parity: WHICH REAL-WORLD IMAGE MODIFICATIONS DOES
3// OUR ENGINE ACTUALLY SURVIVE, AND WHICH DOES IT NOT?
4//
5// TinEye's whole product claim is finding MODIFIED copies -- resized, cropped, edited, colour-adjusted,
6// watermarked. Google Lens adds semantic "looks like". Our engine's near-duplicate tier is a 64-bit
7// GLOBAL perceptual hash (nx_phash dHash) and its looks-alike tier is an 80-dim integer edge-orientation
8// descriptor (nx_visdesc, MPEG-7 EHD / GIST lineage). Both are GLOBAL descriptors. Global descriptors
9// have a known, structural blind spot -- geometry. This ruler MEASURES exactly how big that blind spot
10// is instead of asserting it, and emits a GAP QUEUE ranked worst-first that IS the build order for the
11// next rung (the same discipline the gamebench ruler uses for the game lane).
12//
13// METHOD (liar-killed by construction):
14// * a deterministic corpus of NCORP procedurally-generated multi-scale images (coarse blob field +
15// a structural layer + fine detail) -- distinct, reproducible, no network, no fixtures on disk.
16// * NQ of them are QUERIES. Each query is put through all 16 nx_imgxform classes.
17// * for every (query, class): rank the transformed image against ALL NCORP originals and check
18// whether the true source ranks #1. That is recall@1 -- the metric a reverse-image engine lives on.
19// * ALSO report det@THRESH: would the LIVE client (threshold 10 Hamming) have returned it at all?
20// recall@1 can be 1000 while det@10 is 0 -- "nearest but not near enough" is a silent miss in prod.
21// * dHash (Hamming) and visdesc (L1) are measured HEAD-TO-HEAD on identical inputs, so the decision
22// to wire the unwired descriptor tier is made on data, not taste.
23//
24// HONESTY BOUND, stated in the output: recall@1 against NCORP distractors is an UPPER BOUND on
25// real-web performance. A production corpus has 10^8..10^10 distractors; a class scoring below ~900
26// here is already unusable, and a class scoring 1000 here is merely "not yet disproven".
27//
28// Composes nx_imgxform (deform) + nx_phash (hash) + nx_visdesc (describe) -- ZERO new ranking math.
29// license_tier: ORIGINAL
30import "nx_imgxform.nx"
31import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
32import "nx_imgcorpus.nx"
33import "nx_imgsearch_engine.nx"
34import "nx_phash.nx"
35import "nx_visdesc.nx"
36
37const IB_NCORP: i64 = 256 // corpus / distractor set size
38const IB_NQ: i64 = 32 // queries put through the battery
39const IB_W: i64 = 64 // corpus image width
40const IB_H: i64 = 64 // corpus image height
41const IB_THRESH: i64 = 10 // the LIVE nx_image_search default dHash threshold (IS_DEFAULT_THRESH)
42const IB_PARITY: i64 = 900 // permille recall@1 at/above which a class counts as "survived"
43
44func b_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
45// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
46// per call and never freed it. At page granularity that is 4096B leaked PER CALL -- the
47// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff. A BENCH is the worst home for
48// it: its purpose is millions of iterations. nxi_* is MSB-first and allocates NOTHING.
49func b_num(v: i64) -> i64 { nxi_out(v); return 0 }
50func b_w(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 }
51// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
52// per call and never freed it. At page granularity that is 4096B leaked PER CALL -- the
53// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff. A BENCH is the worst home for
54// it: its purpose is millions of iterations. nxi_* is MSB-first and allocates NOTHING.
55func b_wn(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }
56
57// right-align v in a field of `wid` columns (report tables must line up to be readable)
58func b_pad(v: i64, wid: i64) -> i64 {
59 var d: i64 = 1
60 var m: i64 = v
61 if m < 0 { m = 0 - m; d = d + 1 }
62 var t: i64 = m / 10
63 while t > 0 { d = d + 1; t = t / 10 }
64 var s: i64 = wid - d
65 while s > 0 { b_puts(" " as *u8); s = s - 1 }
66 b_num(v)
67 return 0
68}
69
70// permille of num/den, guarding den==0 (an empty denominator reports 0, never a divide fault)
71func b_permille(num: i64, den: i64) -> i64 { if den <= 0 { return 0 } return num * 1000 / den }
72
73func main() -> i64 {
74 b_puts("=== NX-IMGBENCH -- reverse-image robustness ruler ===\n" as *u8)
75 b_puts("corpus=" as *u8); b_num(IB_NCORP)
76 b_puts(" distractors queries=" as *u8); b_num(IB_NQ)
77 b_puts(" size=" as *u8); b_num(IB_W); b_puts("x" as *u8); b_num(IB_H)
78 b_puts(" live-threshold=" as *u8); b_num(IB_THRESH); b_puts(" Hamming\n\n" as *u8)
79
80 // ---- build the corpus + its reference descriptors ----
81 let imgs: *i64 = sys_mmap(8 * IB_NCORP) as *i64 // pointer to each image's gray buffer
82 let cdh: *i64 = sys_mmap(8 * IB_NCORP) as *i64 // dHash per corpus image
83 let cvd: *i64 = sys_mmap(8 * IB_NCORP) as *i64 // pointer to each 80-dim visdesc
84 var i: i64 = 0
85 while i < IB_NCORP {
86 let buf: *u8 = nx_imgcorpus_new(i, IB_W, IB_H)
87 imgs[i] = buf as i64
88 cdh[i] = nx_phash_dhash(buf, IB_W, IB_H)
89 let dsc: *i64 = sys_mmap(8 * 80) as *i64
90 nx_visdesc_extract(buf, IB_W, IB_H, dsc)
91 cvd[i] = dsc as i64
92 i = i + 1
93 }
94
95 // corpus separation sanity: mean pairwise dHash Hamming over a diagonal sample. A degenerate
96 // corpus (all images alike) would inflate every score, so this is reported, not assumed.
97 var sep_sum: i64 = 0
98 var sep_n: i64 = 0
99 i = 0
100 while i < IB_NCORP {
101 let j: i64 = (i + 37) % IB_NCORP
102 if j != i { sep_sum = sep_sum + nx_simhash_hamming(cdh[i], cdh[j]); sep_n = sep_n + 1 }
103 i = i + 1
104 }
105 var sep: i64 = 0
106 if sep_n > 0 { sep = sep_sum / sep_n }
107
108 // ---- the ENGINE under test: the shipped multi-tier configuration, indexed on the same corpus.
109 // Measuring the raw descriptors AND the assembled engine on identical inputs is what turns
110 // "we added a tier" into "the tier is worth what we claimed".
111 let eng: *nx_imgengine = nx_imgengine_new(IB_NCORP)
112 nx_imgengine_add_tier(eng, nx_imgtier_new(NX_IT_KIND_COPY, NX_IT_COPY_THRESH, 1000))
113 nx_imgengine_add_tier(eng, nx_imgtier_new(NX_IT_KIND_ORIENT, NX_IT_COPY_THRESH, 1000))
114 nx_imgengine_add_tier(eng, nx_imgtier_new(NX_IT_KIND_LOCAL, NX_IT_LOCAL_THRESH, 900))
115 nx_imgengine_add_tier(eng, nx_imgtier_new(NX_IT_KIND_SIMILAR, NX_IT_SIMILAR_THRESH, 800))
116 i = 0
117 while i < IB_NCORP { nx_imgengine_add_image(eng, imgs[i] as *u8, 0 as *u8, IB_W, IB_H, i); i = i + 1 }
118
119 // ---- per-class accumulators ----
120 let rec_d: *i64 = sys_mmap(8 * XF_COUNT) as *i64 // dHash recall@1 hits
121 let rec_v: *i64 = sys_mmap(8 * XF_COUNT) as *i64 // visdesc recall@1 hits
122 let rec_e: *i64 = sys_mmap(8 * XF_COUNT) as *i64 // ENGINE recall@1 hits
123 let det_d: *i64 = sys_mmap(8 * XF_COUNT) as *i64 // dHash within live threshold
124 let ham_s: *i64 = sys_mmap(8 * XF_COUNT) as *i64 // sum of self-Hamming
125 var k: i64 = 0
126 while k < XF_COUNT { rec_d[k] = 0; rec_v[k] = 0; rec_e[k] = 0; det_d[k] = 0; ham_s[k] = 0; k = k + 1 }
127 let e_pay: *i64 = sys_mmap(8 * 4) as *i64
128 let e_sco: *i64 = sys_mmap(8 * 4) as *i64
129 let e_tie: *i64 = sys_mmap(8 * 4) as *i64
130 let e_dis: *i64 = sys_mmap(8 * 4) as *i64
131
132 let wh: *i64 = sys_mmap(16) as *i64
133 let qd: *i64 = sys_mmap(8 * 80) as *i64
134
135 var t: i64 = 0
136 while t < XF_COUNT {
137 var q: i64 = 0
138 while q < IB_NQ {
139 let src: *u8 = imgs[q] as *u8
140 let tim: *u8 = xf_apply(t, src, IB_W, IB_H, wh)
141 let tw: i64 = wh[0]
142 let th: i64 = wh[1]
143
144 // --- dHash tier: rank the transformed image against every corpus original ---
145 let tdh: i64 = nx_phash_dhash(tim, tw, th)
146 let selfham: i64 = nx_simhash_hamming(tdh, cdh[q])
147 ham_s[t] = ham_s[t] + selfham
148 if selfham <= IB_THRESH { det_d[t] = det_d[t] + 1 }
149 var bi: i64 = 0
150 var bh: i64 = 65
151 var c: i64 = 0
152 while c < IB_NCORP {
153 let hh: i64 = nx_simhash_hamming(tdh, cdh[c])
154 if hh < bh { bh = hh; bi = c }
155 c = c + 1
156 }
157 if bi == q { rec_d[t] = rec_d[t] + 1 }
158
159 // --- visdesc tier: same ranking question, L1 over the 80-dim descriptor ---
160 nx_visdesc_extract(tim, tw, th, qd)
161 var vbi: i64 = 0
162 var vbl: i64 = 0 - 1
163 c = 0
164 while c < IB_NCORP {
165 let l1: i64 = nx_visdesc_l1(qd, cvd[c] as *i64)
166 if vbl < 0 { vbl = l1; vbi = c } else { if l1 < vbl { vbl = l1; vbi = c } }
167 c = c + 1
168 }
169 if vbi == q { rec_v[t] = rec_v[t] + 1 }
170
171 // --- ENGINE tier: the assembled, fused, abstention-gated answer ---
172 let ne: i64 = nx_imgengine_query(eng, tim, 0 as *u8, tw, th, 1, e_pay, e_sco, e_tie, e_dis)
173 if ne > 0 { if e_pay[0] == q { rec_e[t] = rec_e[t] + 1 } }
174
175 q = q + 1
176 }
177 t = t + 1
178 }
179
180 // ---- report ----
181 b_puts("class dhash_r@1 visdesc_r@1 ENGINE_r@1 det@" as *u8); b_num(IB_THRESH)
182 b_puts(" meanham\n" as *u8)
183 b_puts("---------------------------------------------------------------------------\n" as *u8)
184 var sum_d: i64 = 0
185 var sum_v: i64 = 0
186 var sum_e: i64 = 0
187 var survived: i64 = 0
188 var survived_e: i64 = 0
189 t = 0
190 while t < XF_COUNT {
191 let rd: i64 = b_permille(rec_d[t], IB_NQ)
192 let rv: i64 = b_permille(rec_v[t], IB_NQ)
193 let re: i64 = b_permille(rec_e[t], IB_NQ)
194 let dt: i64 = b_permille(det_d[t], IB_NQ)
195 var mh: i64 = 0
196 if IB_NQ > 0 { mh = ham_s[t] / IB_NQ }
197 b_puts(" " as *u8); b_puts(xf_name(t))
198 b_pad(rd, 8); b_pad(rv, 13); b_pad(re, 13); b_pad(dt, 9); b_pad(mh, 9)
199 b_puts("\n" as *u8)
200 sum_d = sum_d + rd
201 sum_v = sum_v + rv
202 sum_e = sum_e + re
203 if rd >= IB_PARITY { survived = survived + 1 }
204 if re >= IB_PARITY { survived_e = survived_e + 1 }
205 t = t + 1
206 }
207 b_puts("---------------------------------------------------------------------------\n" as *u8)
208 b_puts("corpus separation (mean pairwise dHash Hamming, distinct images) = " as *u8); b_num(sep)
209 b_puts(" of 64\n" as *u8)
210 b_puts("OVERALL dhash=" as *u8); b_num(sum_d / XF_COUNT)
211 b_puts(" visdesc=" as *u8); b_num(sum_v / XF_COUNT)
212 b_puts(" ENGINE=" as *u8); b_num(sum_e / XF_COUNT)
213 b_puts(" permille\n" as *u8)
214 b_puts("CLASSES SURVIVED (r@1 >= " as *u8); b_num(IB_PARITY); b_puts(" permille): single-hash " as *u8)
215 b_num(survived); b_puts(" of " as *u8); b_num(XF_COUNT)
216 b_puts(" -> ENGINE " as *u8); b_num(survived_e); b_puts(" of " as *u8); b_num(XF_COUNT)
217 b_puts("\n\n" as *u8)
218
219 // ---- GAP QUEUE: worst ENGINE class first. This ordering IS the build order for the next rung. ----
220 b_puts("GAP-QUEUE (worst ENGINE class first -- this ordering IS the build order):\n" as *u8)
221 let done: *u8 = sys_mmap(XF_COUNT)
222 k = 0
223 while k < XF_COUNT { done[k] = 0 as u8; k = k + 1 }
224 var rank: i64 = 0
225 while rank < XF_COUNT {
226 var best: i64 = 0 - 1
227 var bestv: i64 = 0
228 t = 0
229 while t < XF_COUNT {
230 if done[t] == (0 as u8) {
231 let re: i64 = b_permille(rec_e[t], IB_NQ)
232 if best < 0 { best = t; bestv = re } else { if re < bestv { best = t; bestv = re } }
233 }
234 t = t + 1
235 }
236 if best >= 0 {
237 done[best] = 1 as u8
238 if bestv < IB_PARITY {
239 b_puts(" " as *u8); b_pad(rank + 1, 2); b_puts(". " as *u8); b_puts(xf_name(best))
240 b_puts(" ENGINE=" as *u8); b_pad(bestv, 4)
241 b_puts(" (dhash=" as *u8); b_pad(b_permille(rec_d[best], IB_NQ), 4)
242 b_puts(" visdesc=" as *u8); b_pad(b_permille(rec_v[best], IB_NQ), 4)
243 b_puts(")\n" as *u8)
244 }
245 }
246 rank = rank + 1
247 }
248
249 // ---- integrity verdict: the ruler must not lie about itself ----
250 // identity MUST be a perfect round trip on both tiers, and the corpus MUST be separated;
251 // if either fails the measurement above is meaningless and the ruler says so.
252 var pass: i64 = 0
253 let rows: i64 = 3
254 b_puts("\n---- ruler integrity ----\n" as *u8)
255 b_puts(" row1 identity dhash r@1 == 1000 and det == 1000 -> " as *u8)
256 if rec_d[XF_IDENT] == IB_NQ { if det_d[XF_IDENT] == IB_NQ { pass = pass + 1; b_puts("PASS\n" as *u8) } else { b_puts("FAIL\n" as *u8) } } else { b_puts("FAIL\n" as *u8) }
257 b_puts(" row2 identity visdesc r@1 == 1000 -> " as *u8)
258 if rec_v[XF_IDENT] == IB_NQ { pass = pass + 1; b_puts("PASS\n" as *u8) } else { b_puts("FAIL\n" as *u8) }
259 b_puts(" row3 corpus separated (mean pairwise Hamming >= 16 of 64) -> " as *u8)
260 if sep >= 16 { pass = pass + 1; b_puts("PASS\n" as *u8) } else { b_puts("FAIL\n" as *u8) }
261
262 let lg: i64 = sys_openat_append("knowledge/status/imgbench.log" as *u8, 0x1a4)
263 if lg >= 0 {
264 b_w(lg, "IMGBENCH corpus=" as *u8); b_wn(lg, IB_NCORP)
265 b_w(lg, " queries=" as *u8); b_wn(lg, IB_NQ)
266 b_w(lg, " classes=" as *u8); b_wn(lg, XF_COUNT)
267 b_w(lg, " survived=" as *u8); b_wn(lg, survived)
268 b_w(lg, " survived_engine=" as *u8); b_wn(lg, survived_e)
269 b_w(lg, " dhash_permille=" as *u8); b_wn(lg, sum_d / XF_COUNT)
270 b_w(lg, " visdesc_permille=" as *u8); b_wn(lg, sum_v / XF_COUNT)
271 b_w(lg, " engine_permille=" as *u8); b_wn(lg, sum_e / XF_COUNT)
272 b_w(lg, " separation=" as *u8); b_wn(lg, sep)
273 b_w(lg, " rows=" as *u8); b_wn(lg, rows)
274 b_w(lg, " pass=" as *u8); b_wn(lg, pass)
275 if pass == rows { b_w(lg, " verdict=GREEN\n" as *u8) } else { b_w(lg, " verdict=RED\n" as *u8) }
276 sys_close(lg)
277 }
278
279 b_puts("IMGBENCH rows=" as *u8); b_num(rows); b_puts(" pass=" as *u8); b_num(pass); b_puts("\n" as *u8)
280 b_puts("HONESTY BOUND: recall@1 measured against " as *u8); b_num(IB_NCORP)
281 b_puts(" distractors is an UPPER BOUND on real-web\nperformance (a production corpus has 10^8+). A class below " as *u8)
282 b_num(IB_PARITY); b_puts(" permille here is already unusable.\n" as *u8)
283 if pass == rows { b_puts("IMGBENCH verdict=GREEN\n" as *u8); sys_exit(0); return 0 }
284 b_puts("IMGBENCH verdict=RED\n" as *u8)
285 sys_exit(1)
286 return 1
287}