code wiki / (root) / nx_imgbench.nx

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}