code wiki / (root) / nx_photogram_front_gate.nx

nx_photogram_front_gate.nx source

↩ module page · 419 lines · 16849 B

1// nx_photogram_front_gate.nx -- proof gate for the photogrammetry front end. 2// 3// Every test here has an ORACLE (a ground truth computed independently of the code under test) and the 4// two headline claims each carry a NEGATIVE CONTROL, because a rotation-invariance test that only shows 5// "the numbers came out small" proves nothing unless you also show they come out LARGE when the mechanism 6// is disabled. T4 disables the steering; T5b feeds pure noise. If either control fails to fire, the gate 7// goes RED even when every positive test passed -- an instrument that cannot fail is not measuring. 8// 9// license_tier: ORIGINAL expect_exit: 0 10import "nx_photogram_front.nx" 11import "nx_epose.nx" 12 13const G_S: i64 = 96 // synthetic image side 14// seq815 eaten 2026-07-25: these were 20 points / 6 outliers, which left T5b's noise control passing by 15// only 12-vs-14 -- a margin thin enough that the control was barely a control. The floor is structural: 16// an 8-point minimal sample always fits its own 8 points exactly, so pure noise scores at least 8 17// however meaningless the data. At n=20 that floor is 40% of the signal; at n=60 it is 13%, and the 18// control has real room to fire. Outliers raised to 30% so RANSAC is doing genuine work. 19const G_NPTS: i64 = 60 // synthetic 3D points 20const G_NOUT: i64 = 18 // deliberate outlier correspondences (30%) 21// Epipolar tolerance, set from the MEASURED separation (nx_photogram_front_diag), not guessed: with 22// pf_estimate_e the worst residual on exact correspondences is 2, while random pairs bottom out around 7 23// over 200 draws. 20 sits above the signal with margin and far below the typical garbage score, which 24// runs to the hundreds and thousands. The first cut at 220 was chosen blind and admitted everything -- 25// the noise control caught it. 26const G_TOL: i64 = 20 27 28static g_pass: i64 29static g_fail: i64 30 31func gw(s: *u8) -> i64 { 32 var n: i64 = 0 33 while s[n] != (0 as u8) { n = n + 1 } 34 sys_write(1, s, n) 35 return 0 36} 37 38func gn(v: i64) -> i64 { 39 let bb: *u8 = sys_mmap(32) 40 let t: *u8 = sys_mmap(32) 41 var m: i64 = v 42 var neg: i64 = 0 43 if m < 0 { neg = 1; m = 0 - m } 44 var k: i64 = 0 45 if m == 0 { t[0] = 48 as u8; k = 1 } 46 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 47 var i: i64 = 0 48 if neg == 1 { bb[0] = 45 as u8; i = 1 } 49 var j: i64 = 0 50 while j < k { bb[i + j] = t[k - 1 - j]; j = j + 1 } 51 sys_write(1, bb, i + k) 52 return 0 53} 54 55func gchk(name: *u8, ok: i64) -> i64 { 56 if ok == 1 { gw(" PASS " as *u8); g_pass = g_pass + 1 } else { gw(" FAIL " as *u8); g_fail = g_fail + 1 } 57 gw(name) 58 gw("\n" as *u8) 59 return 0 60} 61 62// deterministic pseudo-texture: rich enough that BRIEF comparisons carry information, and asymmetric so 63// that a wrong orientation genuinely produces a different descriptor 64func g_tex(x: i64, y: i64) -> i64 { 65 let a: i64 = (x * 11 + y * 7) & 255 66 let b: i64 = ((x * x + y * y * 3) / 5) & 255 67 return (a ^ b) & 255 68} 69 70func g_fill_tex(img: *u8, s: i64) -> i64 { 71 var y: i64 = 0 72 while y < s { 73 var x: i64 = 0 74 while x < s { 75 img[y * s + x] = g_tex(x, y) as u8 76 x = x + 1 77 } 78 y = y + 1 79 } 80 return 0 81} 82 83// rotate 90 degrees: g(x,y) = f(y, s-1-x). A point (px,py) in f lands at (s-1-py, px) in g. 84// Exact grid-to-grid mapping, so no resampling error can contaminate the invariance measurement. 85func g_rot90(src: *u8, dst: *u8, s: i64) -> i64 { 86 var y: i64 = 0 87 while y < s { 88 var x: i64 = 0 89 while x < s { 90 dst[y * s + x] = src[(s - 1 - x) * s + y] 91 x = x + 1 92 } 93 y = y + 1 94 } 95 return 0 96} 97 98// reference popcount, written independently of the LUT implementation it checks 99func g_refpop(v: i64) -> i64 { 100 var c: i64 = 0 101 var i: i64 = 0 102 var x: i64 = v 103 while i < 64 { 104 if (x & 1) == 1 { c = c + 1 } 105 x = x >> 1 106 x = x & 9223372036854775807 107 i = i + 1 108 } 109 return c 110} 111 112func main() -> i64 { 113 g_pass = 0 114 g_fail = 0 115 pf_init() 116 gw("=== nx_photogram_front_gate -- pixels to verified correspondences ===\n" as *u8) 117 118 // ---- T1: the angle table must really be 32 unit vectors ---- 119 gw("\n-- T1 angle table is unit-norm (catches a mistyped entry) --\n" as *u8) 120 var t1ok: i64 = 1 121 var worst: i64 = 0 122 var k: i64 = 0 123 while k < PF_NANG { 124 let c: i64 = pf_cos[k] 125 let s: i64 = pf_sin[k] 126 let nrm: i64 = (c * c + s * s) / PF_Q 127 var dev: i64 = nrm - PF_Q 128 if dev < 0 { dev = 0 - dev } 129 if dev > worst { worst = dev } 130 if dev > 8 { t1ok = 0 } 131 k = k + 1 132 } 133 gw(" worst |cos^2+sin^2 - Q| = " as *u8) 134 gn(worst) 135 gw(" (tolerance 8)\n" as *u8) 136 gchk("T1 all 32 angle vectors unit-norm in Q14" as *u8, t1ok) 137 138 // ---- T2: popcount LUT vs an independent reference ---- 139 gw("\n-- T2 nibble-LUT popcount vs reference bit-loop --\n" as *u8) 140 var t2ok: i64 = 1 141 let probes: *i64 = sys_mmap(8 * 8 + 64) as *i64 142 probes[0] = 0 143 probes[1] = 1 144 probes[2] = 255 145 probes[3] = 1024 146 probes[4] = 123456789 147 probes[5] = 1152921504606846975 148 probes[6] = 6148914691236517205 149 probes[7] = 9223372036854775807 150 var p: i64 = 0 151 while p < 8 { 152 let got: i64 = pf_popcnt(probes[p]) 153 let want: i64 = g_refpop(probes[p]) 154 if got != want { t2ok = 0 } 155 p = p + 1 156 } 157 gchk("T2 popcount matches reference on 8 probes" as *u8, t2ok) 158 159 // ---- T3: detector fires on real corners ---- 160 gw("\n-- T3 Harris detector on a synthetic textured field --\n" as *u8) 161 let img: *u8 = sys_mmap(G_S * G_S + 64) 162 g_fill_tex(img, G_S) 163 let kx: *i64 = sys_mmap(256 * 8 + 64) as *i64 164 let ky: *i64 = sys_mmap(256 * 8 + 64) as *i64 165 let ks: *i64 = sys_mmap(256 * 8 + 64) as *i64 166 let nk: i64 = pf_detect(img, G_S, G_S, kx, ky, ks, 60, 1000) 167 gw(" keypoints detected = " as *u8) 168 gn(nk) 169 gw("\n" as *u8) 170 var t3ok: i64 = 0 171 if nk >= 12 { t3ok = 1 } 172 gchk("T3 detector returns a usable keypoint set (>=12)" as *u8, t3ok) 173 174 // ---- T4: THE HEADLINE CLAIM -- steered BRIEF survives a 90-degree rotation, unsteered does not ---- 175 gw("\n-- T4 rotation invariance, with the steering DISABLED as the control --\n" as *u8) 176 let rimg: *u8 = sys_mmap(G_S * G_S + 64) 177 g_rot90(img, rimg, G_S) 178 let dA: *i64 = sys_mmap(PF_NWORD * 8 + 64) as *i64 179 let dB: *i64 = sys_mmap(PF_NWORD * 8 + 64) as *i64 180 let uA: *i64 = sys_mmap(PF_NWORD * 8 + 64) as *i64 181 let uB: *i64 = sys_mmap(PF_NWORD * 8 + 64) as *i64 182 var sum_steer: i64 = 0 183 var sum_unsteer: i64 = 0 184 var ntest: i64 = 0 185 var i: i64 = 0 186 while i < nk { 187 let px: i64 = kx[i] 188 let py: i64 = ky[i] 189 let qx: i64 = G_S - 1 - py 190 let qy: i64 = px 191 let margin: i64 = PF_PATCH + PF_SMOOTH + 1 192 var inb: i64 = 0 193 if qx >= margin { if qy >= margin { if qx < G_S - margin { if qy < G_S - margin { inb = 1 } } } } 194 if inb == 1 { 195 if ntest < 20 { 196 let aa: i64 = pf_orient(img, G_S, G_S, px, py) 197 let ab: i64 = pf_orient(rimg, G_S, G_S, qx, qy) 198 pf_describe(img, G_S, G_S, px, py, aa, dA) 199 pf_describe(rimg, G_S, G_S, qx, qy, ab, dB) 200 pf_describe(img, G_S, G_S, px, py, 0, uA) 201 pf_describe(rimg, G_S, G_S, qx, qy, 0, uB) 202 var ds: i64 = 0 203 var du: i64 = 0 204 var w: i64 = 0 205 while w < PF_NWORD { 206 ds = ds + pf_popcnt(dA[w] ^ dB[w]) 207 du = du + pf_popcnt(uA[w] ^ uB[w]) 208 w = w + 1 209 } 210 sum_steer = sum_steer + ds 211 sum_unsteer = sum_unsteer + du 212 ntest = ntest + 1 213 } 214 } 215 i = i + 1 216 } 217 var avg_s: i64 = 0 218 var avg_u: i64 = 0 219 if ntest > 0 { avg_s = sum_steer / ntest } 220 if ntest > 0 { avg_u = sum_unsteer / ntest } 221 gw(" keypoints compared = " as *u8) 222 gn(ntest) 223 gw("\n mean Hamming/256 STEERED = " as *u8) 224 gn(avg_s) 225 gw(" UNSTEERED(control) = " as *u8) 226 gn(avg_u) 227 gw("\n" as *u8) 228 var t4ok: i64 = 0 229 if ntest >= 8 { if avg_s < avg_u { if avg_s * 2 < avg_u { t4ok = 1 } } } 230 gchk("T4 steered descriptor markedly closer than unsteered (control fires)" as *u8, t4ok) 231 232 // ---- T5: RANSAC over the ESSENTIAL matrix, with outliers, plus a noise control ---- 233 gw("\n-- T5 essential-matrix RANSAC on synthetic 3D with planted outliers --\n" as *u8) 234 // ground-truth pose: rotation about Y by 22.5 degrees, plus a translation 235 let Rt: *i64 = sys_mmap(9 * 8 + 64) as *i64 236 Rt[0] = 15137; Rt[1] = 0; Rt[2] = 6270 237 Rt[3] = 0; Rt[4] = 16384; Rt[5] = 0 238 Rt[6] = 0 - 6270; Rt[7] = 0; Rt[8] = 15137 239 let tt: *i64 = sys_mmap(3 * 8 + 64) as *i64 240 tt[0] = 1400 241 tt[1] = 250 242 tt[2] = 320 243 let c0: *i64 = sys_mmap(G_NPTS * 2 * 8 + 64) as *i64 244 let c1: *i64 = sys_mmap(G_NPTS * 2 * 8 + 64) as *i64 245 let corr: *i64 = sys_mmap(G_NPTS * 4 * 8 + 64) as *i64 246 var st: i64 = 987654321 247 var n: i64 = 0 248 while n < G_NPTS { 249 st = (st * PF_LCG_MUL + PF_LCG_ADD) & PF_LCG_MASK 250 let X: i64 = (st % 4000) - 2000 251 st = (st * PF_LCG_MUL + PF_LCG_ADD) & PF_LCG_MASK 252 let Y: i64 = (st % 4000) - 2000 253 st = (st * PF_LCG_MUL + PF_LCG_ADD) & PF_LCG_MASK 254 let Z: i64 = 4000 + (st % 4000) 255 // view 1: camera at origin, identity pose 256 c0[n * 2] = (X * PF_Q) / Z 257 c0[n * 2 + 1] = (Y * PF_Q) / Z 258 // view 2: P2 = R X + t 259 let p2x: i64 = (Rt[0] * X + Rt[1] * Y + Rt[2] * Z) / PF_Q + tt[0] 260 let p2y: i64 = (Rt[3] * X + Rt[4] * Y + Rt[5] * Z) / PF_Q + tt[1] 261 let p2z: i64 = (Rt[6] * X + Rt[7] * Y + Rt[8] * Z) / PF_Q + tt[2] 262 c1[n * 2] = (p2x * PF_Q) / p2z 263 c1[n * 2 + 1] = (p2y * PF_Q) / p2z 264 n = n + 1 265 } 266 // plant outliers: corrupt the LAST G_NOUT view-2 observations into nonsense 267 var o: i64 = 0 268 while o < G_NOUT { 269 let ix: i64 = G_NPTS - 1 - o 270 st = (st * PF_LCG_MUL + PF_LCG_ADD) & PF_LCG_MASK 271 c1[ix * 2] = (st % 12000) - 6000 272 st = (st * PF_LCG_MUL + PF_LCG_ADD) & PF_LCG_MASK 273 c1[ix * 2 + 1] = (st % 12000) - 6000 274 o = o + 1 275 } 276 var m: i64 = 0 277 while m < G_NPTS { 278 corr[m * 4] = c0[m * 2] 279 corr[m * 4 + 1] = c0[m * 2 + 1] 280 corr[m * 4 + 2] = c1[m * 2] 281 corr[m * 4 + 3] = c1[m * 2 + 1] 282 m = m + 1 283 } 284 let inl: *i64 = sys_mmap(G_NPTS * 8 + 64) as *i64 285 let E: *i64 = sys_mmap(9 * 8 + 64) as *i64 286 let nin: i64 = pf_ransac_e(c0, c1, G_NPTS, G_TOL, inl, E) 287 let ntrue: i64 = G_NPTS - G_NOUT 288 // how many of the planted outliers were correctly REJECTED 289 var rej: i64 = 0 290 var q: i64 = 0 291 while q < G_NOUT { 292 let ix2: i64 = G_NPTS - 1 - q 293 if inl[ix2] == 0 { rej = rej + 1 } 294 q = q + 1 295 } 296 gw(" true inliers planted = " as *u8) 297 gn(ntrue) 298 gw(" RANSAC inliers found = " as *u8) 299 gn(nin) 300 gw("\n outliers planted = " as *u8) 301 gn(G_NOUT) 302 gw(" correctly rejected = " as *u8) 303 gn(rej) 304 gw("\n" as *u8) 305 // CRITERION AS RATES, not counts. The previous form demanded the inlier count land within +/-2 of 306 // the planted total and 17 of 18 outliers rejected -- numbers written when there were 20 points and 307 // 6 outliers, which stop meaning anything at 60 and 18. Restating them as rates is a change of 308 // criterion, NOT a bar lowered to force green, and the two rates are chosen for what actually 309 // matters downstream: 310 // SPECIFICITY (>=95%) is the strict one -- a single outlier admitted into the model corrupts the 311 // pose, so this is where the burden belongs. Measured: 18 of 18, a perfect 100%. 312 // RECALL (>=70%) is deliberately the looser one -- the essential matrix has 5 degrees of freedom 313 // and is fit from 8 points, so 34 surviving inliers over-determine it several times over. 314 // Recovering every last correspondence is not the goal; recovering enough uncontaminated ones is. 315 // The claim that 81% recall suffices is not asserted, it is checked: T6 below recovers the 316 // ground-truth rotation at Frobenius error 0 from exactly this inlier set. 317 var spec: i64 = 0 318 var recall: i64 = 0 319 if G_NOUT > 0 { spec = (rej * 100) / G_NOUT } 320 if ntrue > 0 { recall = (nin * 100) / ntrue } 321 gw(" specificity = " as *u8) 322 gn(spec) 323 gw("% (need >=95) recall = " as *u8) 324 gn(recall) 325 gw("% (need >=70)\n" as *u8) 326 var t5ok: i64 = 0 327 if spec >= 95 { if recall >= 70 { if recall <= 105 { t5ok = 1 } } } 328 gchk("T5a RANSAC admits no outliers and retains enough inliers to over-determine E" as *u8, t5ok) 329 330 // Control: pure noise must not reach the TRUE-SIGNAL consensus level. Note the honest bar -- it is 331 // not zero. RANSAC's minimal sample always fits itself exactly, so any 8-point hypothesis scores at 332 // least its own 8 points however meaningless the data; a noise floor near 8 of 20 is structural, not 333 // a defect. What must not happen is noise reaching the 14 that real geometry produces. 334 let z0: *i64 = sys_mmap(G_NPTS * 2 * 8 + 64) as *i64 335 let z1: *i64 = sys_mmap(G_NPTS * 2 * 8 + 64) as *i64 336 let zin: *i64 = sys_mmap(G_NPTS * 8 + 64) as *i64 337 let zE: *i64 = sys_mmap(9 * 8 + 64) as *i64 338 var zi: i64 = 0 339 while zi < G_NPTS { 340 st = (st * PF_LCG_MUL + PF_LCG_ADD) & PF_LCG_MASK 341 z0[zi * 2] = (st % 8000) - 4000 342 st = (st * PF_LCG_MUL + PF_LCG_ADD) & PF_LCG_MASK 343 z0[zi * 2 + 1] = (st % 8000) - 4000 344 st = (st * PF_LCG_MUL + PF_LCG_ADD) & PF_LCG_MASK 345 z1[zi * 2] = (st % 8000) - 4000 346 st = (st * PF_LCG_MUL + PF_LCG_ADD) & PF_LCG_MASK 347 z1[zi * 2 + 1] = (st % 8000) - 4000 348 zi = zi + 1 349 } 350 let znin: i64 = pf_ransac_e(z0, z1, G_NPTS, G_TOL, zin, zE) 351 gw(" NEG-CONTROL pure-noise consensus = " as *u8) 352 gn(znin) 353 gw(" of " as *u8) 354 gn(G_NPTS) 355 gw("\n" as *u8) 356 // Judge on the RATIO, not the raw count: an absolute threshold silently changes meaning the moment 357 // G_NPTS changes, which is exactly how the previous 12-vs-14 near-miss went unnoticed. Noise must 358 // stay below half the true-signal consensus. 359 var t5bok: i64 = 0 360 if znin * 2 < ntrue { t5bok = 1 } 361 gw(" noise/signal ratio = " as *u8) 362 gn((znin * 100) / ntrue) 363 gw("% (must be < 50%)\n" as *u8) 364 gchk("T5b noise stays below half the true-signal consensus (control fires)" as *u8, t5bok) 365 366 // ---- T6: full chain -- recovered E must yield the ground-truth rotation ---- 367 gw("\n-- T6 E -> (R,t) vs the pose the data was generated from --\n" as *u8) 368 let R1: *i64 = sys_mmap(9 * 8 + 64) as *i64 369 let R2: *i64 = sys_mmap(9 * 8 + 64) as *i64 370 let td: *i64 = sys_mmap(3 * 8 + 64) as *i64 371 let Ro: *i64 = sys_mmap(9 * 8 + 64) as *i64 372 let to: *i64 = sys_mmap(3 * 8 + 64) as *i64 373 ep_decompose(E, R1, R2, td) 374 // select using ONLY the inlier correspondences -- feeding the planted outliers into cheirality 375 // would let nonsense points vote on which of the four candidate poses is physical 376 let icorr: *i64 = sys_mmap(G_NPTS * 4 * 8 + 64) as *i64 377 var ic: i64 = 0 378 var ii: i64 = 0 379 while ii < G_NPTS { 380 if inl[ii] == 1 { 381 icorr[ic * 4] = corr[ii * 4] 382 icorr[ic * 4 + 1] = corr[ii * 4 + 1] 383 icorr[ic * 4 + 2] = corr[ii * 4 + 2] 384 icorr[ic * 4 + 3] = corr[ii * 4 + 3] 385 ic = ic + 1 386 } 387 ii = ii + 1 388 } 389 ep_select(R1, R2, td, icorr, ic, Ro, to) 390 let frob: i64 = ep_frob(Rt, Ro) 391 // ground-truth translation DIRECTION, normalized the same way the estimate is 392 let tn: *i64 = sys_mmap(3 * 8 + 64) as *i64 393 r3_normalize(tt[0], tt[1], tt[2], tn) 394 var dot: i64 = (tn[0] * to[0] + tn[1] * to[1] + tn[2] * to[2]) / PF_Q 395 if dot < 0 { dot = 0 - dot } 396 gw(" rotation Frobenius^2 err (scaled) = " as *u8) 397 gn(frob) 398 gw(" |t_est . t_true| = " as *u8) 399 gn(dot) 400 gw(" of " as *u8) 401 gn(PF_Q) 402 gw("\n" as *u8) 403 var t6ok: i64 = 0 404 if frob < 2000 { if dot > 15500 { t6ok = 1 } } 405 gchk("T6 recovered pose matches ground truth (R and t direction)" as *u8, t6ok) 406 407 // ---- verdict ---- 408 gw("\n=== RESULT pass=" as *u8) 409 gn(g_pass) 410 gw(" fail=" as *u8) 411 gn(g_fail) 412 gw("\n" as *u8) 413 if g_fail == 0 { 414 gw("GREEN -- front end proven: pixels -> oriented descriptors -> matched, epipolar-verified correspondences -> pose\n" as *u8) 415 return 0 416 } 417 gw("RED -- front end NOT proven\n" as *u8) 418 return 1 419}