code wiki / (root) / nx_pnprefine.nx

nx_pnprefine.nx source

↩ module page · 302 lines · 12301 B

1// nx_pnprefine.nx -- Refines camera pose by minimizing reprojection error with a Gauss-Newton linear solve. 2// nx_pnprefine.nx -- GAUSS-NEWTON pose refinement (PnP): given 3D points and their observed 2D projections, 3// refine the camera pose by minimising REPROJECTION error with a real 6x6 linear solve. 4// 5// WHY THIS EXISTS: nx_bundleadjust refines pose by COORDINATE DESCENT over an epipolar cost and is honestly 6// documented as PLATEAUING above machine precision (cost 1270 vs a floor of 67; rotation only ~29% recovered) 7// -- "full metric convergence needs coupled Gauss-Newton/LM (Jacobians + linear solve)" was named as the next 8// rung there. This is that rung. Two structural differences from the coordinate-descent attempt: 9// (1) the cost is REPROJECTION against points held FIXED, not re-triangulated ones. Re-triangulating inside 10// the cost lets the points chase any pose, which is degenerate -- every pose scores well. 11// (2) all six degrees of freedom move TOGETHER from one normal-equation solve, so rotation/translation 12// trade-offs that a per-axis search cannot see are resolved in a single step. 13// Jacobians are NUMERIC (forward differences at a fixed probe step) -- the projection already exists and is 14// exact, so a derived analytic Jacobian would add a second thing to get wrong for no accuracy gain. 15// All integer/fixed-point, so the refinement is bit-reproducible. license_tier: ORIGINAL 16import "nx_recon3d.nx" 17 18const PNP_DT: i64 = 32 // translation probe step, coord units 19const PNP_DW: i64 = 96 // rotation probe step, Q14 radians (~0.0059 rad) 20const PNP_SQ: i64 = 1024 // fixed-point scale for the linear solve 21const PNP_MAXSTEP: i64 = 8 // clamp a single update to 8 probe steps (a runaway solve cannot bolt) 22// ★image displacement a translation probe should produce, in pixels. This is a TWO-SIDED tradeoff and it was 23// MEASURED, not derived: too small and the forward difference drowns in the integer quantisation of the 24// residuals; too large and the linearisation the Jacobian assumes stops holding. On the real as1 bracket with 25// a wide lens, 8 px gave k1 710 permille out while 4 px gave 6 permille -- so bigger is NOT safer. 26const PNP_TARGET_PX: i64 = 4 27 28// âš âš SCENE-SCALE PROBE. PNP_DT is an ABSOLUTE step in world units, chosen when every scene in this lane sat 29// ~1000-3000 units from the camera. Aim the same solver at a real part in real millimetres 48723 units away 30// and a 32-unit camera nudge moves the projection by f*32/48723 -- UNDER ONE PIXEL. The forward-difference 31// column then quantises to 0s and 1s, the normal equations go ill-conditioned, and the solve STOPS CONVERGING 32// WHILE STILL RETURNING A PLAUSIBLE-LOOKING ANSWER. Measured on the real as1 bracket: SSE 3919 -> 2901 and 33// k1 451 -> 152 permille from this one change (nx_realcal_gate). 34// ★THE FLOOR IS WHAT MAKES THIS SAFE TO APPLY EVERYWHERE: at 1000-3000 units dist/100 is 10-30, below 35// PNP_DT, so every existing synthetic scene keeps EXACTLY the old probe of 32 and every existing gate result 36// is bit-identical. Only genuinely large scenes change, and there the old behaviour was broken. 37// ★★THE RULE IS EXPRESSED IN PIXELS, NOT IN WORLD UNITS, because pixels are what the integer residuals are 38// quantised in -- that is the quantity a numeric Jacobian actually needs. A probe moves the image by 39// f*dt/dist, so solving for a target displacement gives dt = dist*TARGET/f. 40// âš FIRST ATTEMPT AT THIS FIX USED dist/100 AND WAS WRONG -- not because it failed at real scale, but because 41// it ALSO engaged at ~3200 units where the old probe was producing a perfectly healthy 12 px of motion. It 42// silently changed four already-correct results (selfcalib far 1536/SSE 63 -> 1555/SSE 77, and three multical 43// probes). A FIX MUST BE SCOPED TO THE REGIME THAT IS ACTUALLY BROKEN; widening it further is not "more 44// safety", it is an unrequested change to working behaviour. With the pixel rule, dist 1000-3000 at f=1200 45// yields 6-20, below PNP_DT, so every existing scene keeps EXACTLY the old probe of 32. 46func pnp_probe_px(dist: i64, f: i64) -> i64 { 47 if f <= 0 { return PNP_DT } 48 var dt: i64 = (dist * PNP_TARGET_PX) / f 49 if dt < PNP_DT { dt = PNP_DT } 50 return dt 51} 52// working distance = camera centre to the centroid of the points it is looking at 53func pnp_dist_to(cam: *i64, p3: *i64, n: i64) -> i64 { 54 if n <= 0 { return 0 } 55 var sx: i64 = 0 56 var sy: i64 = 0 57 var sz: i64 = 0 58 var i: i64 = 0 59 while i < n { 60 sx = sx + p3[i * 3 + 0] 61 sy = sy + p3[i * 3 + 1] 62 sz = sz + p3[i * 3 + 2] 63 i = i + 1 64 } 65 let dx: i64 = sx / n - cam[0] 66 let dy: i64 = sy / n - cam[1] 67 let dz: i64 = sz / n - cam[2] 68 return r3_isqrt(dx * dx + dy * dy + dz * dz) 69} 70// derive the probe from the data itself, so a caller cannot forget to size it 71func pnp_probe_auto(cam: *i64, p3: *i64, n: i64, f: i64) -> i64 { 72 return pnp_probe_px(pnp_dist_to(cam, p3, n), f) 73} 74// residuals r[2k],r[2k+1] = projected - observed, for all n points. Returns the sum of squares. 75func pnp_resid(cx: i64, cy: i64, cz: i64, basis: *i64, f: i64, p3: *i64, obs: *i64, n: i64, r: *i64) -> i64 { 76 let o2: *i64 = sys_mmap(32) as *i64 77 var k: i64 = 0 78 var sse: i64 = 0 79 while k < n { 80 let zc: i64 = r3_project(cx, cy, cz, basis, f, p3[k * 3 + 0], p3[k * 3 + 1], p3[k * 3 + 2], o2) 81 var du: i64 = 0 82 var dv: i64 = 0 83 if zc > 0 { 84 du = o2[0] - obs[k * 2 + 0] 85 dv = o2[1] - obs[k * 2 + 1] 86 } 87 r[k * 2 + 0] = du 88 r[k * 2 + 1] = dv 89 sse = sse + du * du + dv * dv 90 k = k + 1 91 } 92 return sse 93} 94// apply a small world-space rotation w (Q14) to the camera basis rows, then RE-ORTHONORMALISE. Without the 95// re-orthonormalisation the small-angle update slowly shears the basis and it stops being a rotation. 96func pnp_rot(basis: *i64, wx: i64, wy: i64, wz: i64) -> i64 { 97 var i: i64 = 0 98 while i < 3 { 99 let rx: i64 = basis[i * 3 + 0] 100 let ry: i64 = basis[i * 3 + 1] 101 let rz: i64 = basis[i * 3 + 2] 102 basis[i * 3 + 0] = rx + (wy * rz - wz * ry) / R3_Q 103 basis[i * 3 + 1] = ry + (wz * rx - wx * rz) / R3_Q 104 basis[i * 3 + 2] = rz + (wx * ry - wy * rx) / R3_Q 105 i = i + 1 106 } 107 let t3: *i64 = sys_mmap(32) as *i64 108 // forward := normalize(forward); right := normalize(up x forward); up := forward x right 109 r3_normalize(basis[6], basis[7], basis[8], t3) 110 basis[6] = t3[0] 111 basis[7] = t3[1] 112 basis[8] = t3[2] 113 let c3: *i64 = sys_mmap(32) as *i64 114 r3_cross(basis[3], basis[4], basis[5], basis[6], basis[7], basis[8], c3) 115 r3_normalize(c3[0], c3[1], c3[2], t3) 116 basis[0] = t3[0] 117 basis[1] = t3[1] 118 basis[2] = t3[2] 119 r3_cross(basis[6], basis[7], basis[8], basis[0], basis[1], basis[2], c3) 120 r3_normalize(c3[0], c3[1], c3[2], t3) 121 basis[3] = t3[0] 122 basis[4] = t3[1] 123 basis[5] = t3[2] 124 return 0 125} 126// 6x6 Gaussian elimination with partial pivoting; x returned in PNP_SQ fixed point. 0 = singular. 127func pnp_solve6(A: *i64, b: *i64, x: *i64) -> i64 { 128 var col: i64 = 0 129 while col < 6 { 130 var piv: i64 = col 131 var best: i64 = 0 132 var rr: i64 = col 133 while rr < 6 { 134 var a: i64 = A[rr * 6 + col] 135 if a < 0 { a = 0 - a } 136 if a > best { 137 best = a 138 piv = rr 139 } 140 rr = rr + 1 141 } 142 if best == 0 { return 0 } 143 if piv != col { 144 var c1: i64 = 0 145 while c1 < 6 { 146 let tmp: i64 = A[col * 6 + c1] 147 A[col * 6 + c1] = A[piv * 6 + c1] 148 A[piv * 6 + c1] = tmp 149 c1 = c1 + 1 150 } 151 let tb: i64 = b[col] 152 b[col] = b[piv] 153 b[piv] = tb 154 } 155 var r2: i64 = col + 1 156 while r2 < 6 { 157 let m: i64 = (A[r2 * 6 + col] * PNP_SQ) / A[col * 6 + col] 158 var c2: i64 = col 159 while c2 < 6 { 160 A[r2 * 6 + c2] = A[r2 * 6 + c2] - (m * A[col * 6 + c2]) / PNP_SQ 161 c2 = c2 + 1 162 } 163 b[r2] = b[r2] - (m * b[col]) / PNP_SQ 164 r2 = r2 + 1 165 } 166 col = col + 1 167 } 168 var i: i64 = 5 169 while i >= 0 { 170 var s: i64 = b[i] * PNP_SQ 171 var j: i64 = i + 1 172 while j < 6 { 173 s = s - A[i * 6 + j] * x[j] 174 j = j + 1 175 } 176 if A[i * 6 + i] == 0 { return 0 } 177 x[i] = s / A[i * 6 + i] 178 i = i - 1 179 } 180 return 1 181} 182// ONE Gauss-Newton step. cam[0..2] = centre, basis = 3x3. Returns the sum-of-squares BEFORE the step. 183func pnp_step(cam: *i64, basis: *i64, f: i64, p3: *i64, obs: *i64, n: i64, w: *i64) -> i64 { 184 let nr: i64 = n * 2 185 let r0: *i64 = (w[0]) as *i64 186 let rp: *i64 = (w[1]) as *i64 187 let J: *i64 = (w[2]) as *i64 188 let A: *i64 = (w[3]) as *i64 189 let b: *i64 = (w[4]) as *i64 190 let x: *i64 = (w[5]) as *i64 191 let bb: *i64 = (w[6]) as *i64 192 let dt: i64 = pnp_probe_auto(cam, p3, n, f) 193 let sse: i64 = pnp_resid(cam[0], cam[1], cam[2], basis, f, p3, obs, n, r0) 194 var j: i64 = 0 195 while j < 6 { 196 var i2: i64 = 0 197 while i2 < 9 { 198 bb[i2] = basis[i2] 199 i2 = i2 + 1 200 } 201 var ax: i64 = cam[0] 202 var ay: i64 = cam[1] 203 var az: i64 = cam[2] 204 if j == 0 { ax = ax + dt } 205 if j == 1 { ay = ay + dt } 206 if j == 2 { az = az + dt } 207 if j == 3 { pnp_rot(bb, PNP_DW, 0, 0) } 208 if j == 4 { pnp_rot(bb, 0, PNP_DW, 0) } 209 if j == 5 { pnp_rot(bb, 0, 0, PNP_DW) } 210 let ig: i64 = pnp_resid(ax, ay, az, bb, f, p3, obs, n, rp) 211 var k: i64 = 0 212 while k < nr { 213 J[j * nr + k] = rp[k] - r0[k] 214 k = k + 1 215 } 216 j = j + 1 217 } 218 // normal equations: A = J^T J, b = -J^T r0 219 var a1: i64 = 0 220 while a1 < 6 { 221 var a2: i64 = 0 222 while a2 < 6 { 223 var s: i64 = 0 224 var k2: i64 = 0 225 while k2 < nr { 226 s = s + J[a1 * nr + k2] * J[a2 * nr + k2] 227 k2 = k2 + 1 228 } 229 A[a1 * 6 + a2] = s 230 a2 = a2 + 1 231 } 232 var sb: i64 = 0 233 var k3: i64 = 0 234 while k3 < nr { 235 sb = sb + J[a1 * nr + k3] * r0[k3] 236 k3 = k3 + 1 237 } 238 b[a1] = 0 - sb 239 a1 = a1 + 1 240 } 241 if pnp_solve6(A, b, x) == 0 { return sse } 242 // clamp, then apply: translation directly, rotation through the small-angle update 243 var q: i64 = 0 244 while q < 6 { 245 let lim: i64 = PNP_MAXSTEP * PNP_SQ 246 if x[q] > lim { x[q] = lim } 247 if x[q] < 0 - lim { x[q] = 0 - lim } 248 q = q + 1 249 } 250 cam[0] = cam[0] + (x[0] * dt) / PNP_SQ 251 cam[1] = cam[1] + (x[1] * dt) / PNP_SQ 252 cam[2] = cam[2] + (x[2] * dt) / PNP_SQ 253 pnp_rot(basis, (x[3] * PNP_DW) / PNP_SQ, (x[4] * PNP_DW) / PNP_SQ, (x[5] * PNP_DW) / PNP_SQ) 254 return sse 255} 256// iterate; keeps the BEST pose seen so the refinement can never end worse than it started 257func pnp_refine(cam: *i64, basis: *i64, f: i64, p3: *i64, obs: *i64, n: i64, iters: i64, w: *i64) -> i64 { 258 let rtmp: *i64 = (w[0]) as *i64 259 var best: i64 = pnp_resid(cam[0], cam[1], cam[2], basis, f, p3, obs, n, rtmp) 260 let bcam: *i64 = (w[7]) as *i64 261 let bbas: *i64 = (w[8]) as *i64 262 var i: i64 = 0 263 while i < 3 { 264 bcam[i] = cam[i] 265 i = i + 1 266 } 267 i = 0 268 while i < 9 { 269 bbas[i] = basis[i] 270 i = i + 1 271 } 272 var it: i64 = 0 273 while it < iters { 274 let ig: i64 = pnp_step(cam, basis, f, p3, obs, n, w) 275 let cur: i64 = pnp_resid(cam[0], cam[1], cam[2], basis, f, p3, obs, n, rtmp) 276 if cur < best { 277 best = cur 278 i = 0 279 while i < 3 { 280 bcam[i] = cam[i] 281 i = i + 1 282 } 283 i = 0 284 while i < 9 { 285 bbas[i] = basis[i] 286 i = i + 1 287 } 288 } 289 it = it + 1 290 } 291 i = 0 292 while i < 3 { 293 cam[i] = bcam[i] 294 i = i + 1 295 } 296 i = 0 297 while i < 9 { 298 basis[i] = bbas[i] 299 i = i + 1 300 } 301 return best 302}