code wiki / (root) / nx_multical.nx

nx_multical.nx source

↩ module page · 217 lines · 8263 B

1// nx_multical.nx -- Jointly calibrates multiple camera views with a shared focal length and per-view poses using Gauss-Newton optimization. 2// nx_multical.nx -- JOINT multi-view calibration: ONE SHARED focal length plus EVERY camera pose, solved 3// together in a single Gauss-Newton system. 4// 5// WHY: nx_selfcalib measured that a 7-DOF single-view solve reaches the reprojection floor while leaving the 6// focal 28% wrong, and named the remedy in its own closing text -- "a known-geometry target, or 7// AUTO-CALIBRATION ACROSS SEVERAL CAMERA MOTIONS". This is that remedy. The structural difference is not more 8// iterations, it is a SHARED PARAMETER: the same focal must explain every view at once, so the focal/distance 9// trade-off that one view cannot separate has to hold simultaneously against several different camera 10// distances -- and it cannot, unless the focal is right. 11// 12// The parameter vector is 6*nv + 1: six pose degrees of freedom per view, plus the single shared focal as the 13// LAST column of the Jacobian. Sharing is the whole point; a per-view focal would re-create the single-view 14// degeneracy nv times over and fit just as well while meaning nothing. 15// license_tier: ORIGINAL 16import "nx_selfcalib.nx" 17 18const MC_DF: i64 = 4 // focal probe step (pixels) 19const MC_SQ: i64 = 1024 20const MC_MAXSTEP: i64 = 8 21const MC_FMIN: i64 = 20 // outside this band it is not a camera, it is a diverged solve 22const MC_FMAX: i64 = 4000 23const MC_MAXV: i64 = 6 // views; the work buffers below are sized for this 24 25// work buffers. Allocated here rather than demanded from the caller: mc_step needs seven separate arrays 26// whose sizes all depend on nv, and a caller-supplied layout is exactly the kind of silent mismatch nx_cc 27// cannot catch. 28func mc_work() -> *i64 { 29 let w: *i64 = sys_mmap(256) as *i64 30 w[0] = sys_mmap(65536) as i64 // r0 residuals, 2*np*nv 31 w[1] = sys_mmap(65536) as i64 // rp perturbed residuals 32 w[2] = sys_mmap(1048576) as i64 // J (6*nv+1) x (2*np*nv) 33 w[3] = sys_mmap(65536) as i64 // A (6*nv+1)^2 34 w[4] = sys_mmap(4096) as i64 // b 35 w[5] = sys_mmap(4096) as i64 // x 36 w[6] = sys_mmap(256) as i64 // bb one saved 3x3 basis 37 w[7] = sys_mmap(1024) as i64 // best cams 38 w[8] = sys_mmap(2048) as i64 // best bases 39 return w 40} 41// residuals for ALL views against ONE shared focal. cams = 3 per view, bases = 9 per view, 42// obs/r = 2 per (view,point) laid out view-major. Returns the summed sum-of-squares. 43func mc_resid(cams: *i64, bases: *i64, f: i64, p3: *i64, obs: *i64, np: i64, nv: i64, r: *i64) -> i64 { 44 var sse: i64 = 0 45 var v: i64 = 0 46 while v < nv { 47 let ob: *i64 = ((obs as i64) + v * np * 16) as *i64 48 let rr: *i64 = ((r as i64) + v * np * 16) as *i64 49 let bs: *i64 = ((bases as i64) + v * 72) as *i64 50 sse = sse + pnp_resid(cams[v * 3 + 0], cams[v * 3 + 1], cams[v * 3 + 2], bs, f, p3, ob, np, rr) 51 v = v + 1 52 } 53 return sse 54} 55// ONE joint Gauss-Newton step over 6*nv + 1 parameters. fp[0] carries the shared focal in/out. 56func mc_step(cams: *i64, bases: *i64, fp: *i64, p3: *i64, obs: *i64, np: i64, nv: i64, w: *i64) -> i64 { 57 let npar: i64 = 6 * nv + 1 58 let nres: i64 = 2 * np * nv 59 let r0: *i64 = (w[0]) as *i64 60 let rp: *i64 = (w[1]) as *i64 61 let J: *i64 = (w[2]) as *i64 62 let A: *i64 = (w[3]) as *i64 63 let b: *i64 = (w[4]) as *i64 64 let x: *i64 = (w[5]) as *i64 65 let bb: *i64 = (w[6]) as *i64 66 // âš scene-scale probe -- see pnp_probe_for. `cams` already points at view 0, the representative camera. 67 let dt: i64 = pnp_probe_auto(cams, p3, np, fp[0]) 68 let sse: i64 = mc_resid(cams, bases, fp[0], p3, obs, np, nv, r0) 69 70 var j: i64 = 0 71 while j < npar { 72 var af: i64 = fp[0] 73 var vv: i64 = 0 74 var sub: i64 = 0 75 var isf: i64 = 0 76 if j == npar - 1 { 77 isf = 1 78 af = af + MC_DF 79 } 80 if isf == 0 { 81 vv = j / 6 82 sub = j % 6 83 } 84 // perturb IN PLACE, evaluate, then restore. Translation restores exactly by subtraction; 85 // âš rotation does NOT -- pnp_rot re-orthonormalises, so the basis must be restored from a saved copy. 86 var saved: i64 = 0 87 if isf == 0 { 88 if sub < 3 { 89 saved = cams[vv * 3 + sub] 90 cams[vv * 3 + sub] = saved + dt 91 } 92 if sub >= 3 { 93 var i2: i64 = 0 94 while i2 < 9 { 95 bb[i2] = bases[vv * 9 + i2] 96 i2 = i2 + 1 97 } 98 let bs: *i64 = ((bases as i64) + vv * 72) as *i64 99 if sub == 3 { pnp_rot(bs, PNP_DW, 0, 0) } 100 if sub == 4 { pnp_rot(bs, 0, PNP_DW, 0) } 101 if sub == 5 { pnp_rot(bs, 0, 0, PNP_DW) } 102 } 103 } 104 let ig: i64 = mc_resid(cams, bases, af, p3, obs, np, nv, rp) 105 if isf == 0 { 106 if sub < 3 { cams[vv * 3 + sub] = saved } 107 if sub >= 3 { 108 var i3: i64 = 0 109 while i3 < 9 { 110 bases[vv * 9 + i3] = bb[i3] 111 i3 = i3 + 1 112 } 113 } 114 } 115 var k: i64 = 0 116 while k < nres { 117 J[j * nres + k] = rp[k] - r0[k] 118 k = k + 1 119 } 120 j = j + 1 121 } 122 // normal equations over the joint system 123 var a1: i64 = 0 124 while a1 < npar { 125 var a2: i64 = 0 126 while a2 < npar { 127 var s: i64 = 0 128 var k2: i64 = 0 129 while k2 < nres { 130 s = s + J[a1 * nres + k2] * J[a2 * nres + k2] 131 k2 = k2 + 1 132 } 133 A[a1 * npar + a2] = s 134 a2 = a2 + 1 135 } 136 var sb: i64 = 0 137 var k3: i64 = 0 138 while k3 < nres { 139 sb = sb + J[a1 * nres + k3] * r0[k3] 140 k3 = k3 + 1 141 } 142 b[a1] = 0 - sb 143 a1 = a1 + 1 144 } 145 if sc_solve(A, b, x, npar) == 0 { return sse } 146 var q: i64 = 0 147 while q < npar { 148 let lim: i64 = MC_MAXSTEP * MC_SQ 149 if x[q] > lim { x[q] = lim } 150 if x[q] < 0 - lim { x[q] = 0 - lim } 151 q = q + 1 152 } 153 var v2: i64 = 0 154 while v2 < nv { 155 cams[v2 * 3 + 0] = cams[v2 * 3 + 0] + (x[v2 * 6 + 0] * dt) / MC_SQ 156 cams[v2 * 3 + 1] = cams[v2 * 3 + 1] + (x[v2 * 6 + 1] * dt) / MC_SQ 157 cams[v2 * 3 + 2] = cams[v2 * 3 + 2] + (x[v2 * 6 + 2] * dt) / MC_SQ 158 let bs2: *i64 = ((bases as i64) + v2 * 72) as *i64 159 pnp_rot(bs2, (x[v2 * 6 + 3] * PNP_DW) / MC_SQ, (x[v2 * 6 + 4] * PNP_DW) / MC_SQ, (x[v2 * 6 + 5] * PNP_DW) / MC_SQ) 160 v2 = v2 + 1 161 } 162 var nf: i64 = fp[0] + (x[npar - 1] * MC_DF) / MC_SQ 163 if nf < MC_FMIN { nf = MC_FMIN } 164 if nf > MC_FMAX { nf = MC_FMAX } 165 fp[0] = nf 166 return sse 167} 168// iterate, keeping the BEST (all poses, shared focal) seen, so a refinement can never end worse than it began 169func mc_refine(cams: *i64, bases: *i64, fp: *i64, p3: *i64, obs: *i64, np: i64, nv: i64, iters: i64, w: *i64) -> i64 { 170 let rtmp: *i64 = (w[0]) as *i64 171 let bcam: *i64 = (w[7]) as *i64 172 let bbas: *i64 = (w[8]) as *i64 173 var best: i64 = mc_resid(cams, bases, fp[0], p3, obs, np, nv, rtmp) 174 var bf: i64 = fp[0] 175 var i: i64 = 0 176 while i < nv * 3 { 177 bcam[i] = cams[i] 178 i = i + 1 179 } 180 i = 0 181 while i < nv * 9 { 182 bbas[i] = bases[i] 183 i = i + 1 184 } 185 var it: i64 = 0 186 while it < iters { 187 let ig: i64 = mc_step(cams, bases, fp, p3, obs, np, nv, w) 188 let cur: i64 = mc_resid(cams, bases, fp[0], p3, obs, np, nv, rtmp) 189 if cur < best { 190 best = cur 191 bf = fp[0] 192 i = 0 193 while i < nv * 3 { 194 bcam[i] = cams[i] 195 i = i + 1 196 } 197 i = 0 198 while i < nv * 9 { 199 bbas[i] = bases[i] 200 i = i + 1 201 } 202 } 203 it = it + 1 204 } 205 i = 0 206 while i < nv * 3 { 207 cams[i] = bcam[i] 208 i = i + 1 209 } 210 i = 0 211 while i < nv * 9 { 212 bases[i] = bbas[i] 213 i = i + 1 214 } 215 fp[0] = bf 216 return best 217}