code wiki / (root) / nx_register3d.nx

nx_register3d.nx source

↩ module page · 252 lines · 11025 B

1// nx_register3d.nx -- 3D RIGID REGISTRATION (Horn's absolute-orientation, quaternion method) -- the "align two 2// 3D scans into one frame" capability (cadtwin: fuse multi-view/multi-scan captures of a car into ONE twin; 3// extends nx_pc's 2D ICP to 3D). Given two corresponding 3D point sets P,Q, recovers the rotation R + translation 4// t s.t. R*P + t ~= Q. SVD-FREE: builds Horn's 4x4 symmetric matrix N from the cross-covariance and finds its 5// dominant eigenvector (the optimal rotation quaternion) by POWER ITERATION on a Gershgorin-shifted N (guarantees 6// convergence to the most-positive eigenvalue). All fixed-point Q14, deterministic. Benchmarked by residual RMS. 7// license_tier: ORIGINAL 8import "nx_syscalls.nx" 9import "nx_vecmath.nx" 10 11const RG_Q: i64 = 16384 12 13func rg_isqrt(n: i64) -> i64 { return vm_isqrt(n) } 14func rg_abs(a: i64) -> i64 { if a < 0 { return 0 - a } return a } 15 16// unit quaternion (Q14) -> 3x3 rotation matrix R (Q14, row-major 9). q = [w,x,y,z]. 17func rg_quat_to_r(q: *i64, r: *i64) -> i64 { 18 let w: i64 = q[0] 19 let x: i64 = q[1] 20 let y: i64 = q[2] 21 let z: i64 = q[3] 22 let xx: i64 = (x * x) / RG_Q 23 let yy: i64 = (y * y) / RG_Q 24 let zz: i64 = (z * z) / RG_Q 25 let xy: i64 = (x * y) / RG_Q 26 let xz: i64 = (x * z) / RG_Q 27 let yz: i64 = (y * z) / RG_Q 28 let wx: i64 = (w * x) / RG_Q 29 let wy: i64 = (w * y) / RG_Q 30 let wz: i64 = (w * z) / RG_Q 31 r[0] = RG_Q - 2 * (yy + zz) 32 r[1] = 2 * (xy - wz) 33 r[2] = 2 * (xz + wy) 34 r[3] = 2 * (xy + wz) 35 r[4] = RG_Q - 2 * (xx + zz) 36 r[5] = 2 * (yz - wx) 37 r[6] = 2 * (xz - wy) 38 r[7] = 2 * (yz + wx) 39 r[8] = RG_Q - 2 * (xx + yy) 40 return 0 41} 42// apply R (Q14) + t (coord) to a coord point -> out3 43func rg_apply(r: *i64, tx: i64, ty: i64, tz: i64, px: i64, py: i64, pz: i64, out3: *i64) -> i64 { 44 out3[0] = (r[0] * px + r[1] * py + r[2] * pz) / RG_Q + tx 45 out3[1] = (r[3] * px + r[4] * py + r[5] * pz) / RG_Q + ty 46 out3[2] = (r[6] * px + r[7] * py + r[8] * pz) / RG_Q + tz 47 return 0 48} 49 50// HORN registration. P,Q = coord point arrays stride 3, n points. out_r[9] (Q14), out_t[3] (coord). 51func rg_register(p: *i64, q: *i64, n: i64, out_r: *i64, out_t: *i64) -> i64 { 52 // centroids 53 var pcx: i64 = 0; var pcy: i64 = 0; var pcz: i64 = 0 54 var qcx: i64 = 0; var qcy: i64 = 0; var qcz: i64 = 0 55 var i: i64 = 0 56 while i < n { 57 pcx = pcx + p[i * 3]; pcy = pcy + p[i * 3 + 1]; pcz = pcz + p[i * 3 + 2] 58 qcx = qcx + q[i * 3]; qcy = qcy + q[i * 3 + 1]; qcz = qcz + q[i * 3 + 2] 59 i = i + 1 60 } 61 pcx = pcx / n; pcy = pcy / n; pcz = pcz / n 62 qcx = qcx / n; qcy = qcy / n; qcz = qcz / n 63 // cross-covariance S[a][b] = sum P_a Q_b (centered) 64 var sxx: i64 = 0; var sxy: i64 = 0; var sxz: i64 = 0 65 var syx: i64 = 0; var syy: i64 = 0; var syz: i64 = 0 66 var szx: i64 = 0; var szy: i64 = 0; var szz: i64 = 0 67 i = 0 68 while i < n { 69 let ax: i64 = p[i * 3] - pcx 70 let ay: i64 = p[i * 3 + 1] - pcy 71 let az: i64 = p[i * 3 + 2] - pcz 72 let bx: i64 = q[i * 3] - qcx 73 let by: i64 = q[i * 3 + 1] - qcy 74 let bz: i64 = q[i * 3 + 2] - qcz 75 sxx = sxx + ax * bx; sxy = sxy + ax * by; sxz = sxz + ax * bz 76 syx = syx + ay * bx; syy = syy + ay * by; syz = syz + ay * bz 77 szx = szx + az * bx; szy = szy + az * by; szz = szz + az * bz 78 i = i + 1 79 } 80 // Horn N (4x4 symmetric, raw scale) 81 let nm: *i64 = sys_mmap(16 * 8) as *i64 82 nm[0] = sxx + syy + szz 83 nm[1] = syz - szy; nm[4] = nm[1] 84 nm[2] = szx - sxz; nm[8] = nm[2] 85 nm[3] = sxy - syx; nm[12] = nm[3] 86 nm[5] = sxx - syy - szz 87 nm[6] = sxy + syx; nm[9] = nm[6] 88 nm[7] = szx + sxz; nm[13] = nm[7] 89 nm[10] = 0 - sxx + syy - szz 90 nm[11] = syz + szy; nm[14] = nm[11] 91 nm[15] = 0 - sxx - syy + szz 92 // Gershgorin shift c = max row abs-sum -> N' = N + cI is PSD (dominant eigval = optimal) 93 var c: i64 = 0 94 var rr: i64 = 0 95 while rr < 4 { 96 var s: i64 = 0 97 var cc: i64 = 0 98 while cc < 4 { s = s + rg_abs(nm[rr * 4 + cc]); cc = cc + 1 } 99 if s > c { c = s } 100 rr = rr + 1 101 } 102 nm[0] = nm[0] + c; nm[5] = nm[5] + c; nm[10] = nm[10] + c; nm[15] = nm[15] + c 103 // scale N to ~Q14 (power iteration cares only about direction; keeps products in range) 104 var maxa: i64 = 1 105 i = 0 106 while i < 16 { let a: i64 = rg_abs(nm[i]); if a > maxa { maxa = a } i = i + 1 } 107 let scl: i64 = (maxa / RG_Q) + 1 108 i = 0 109 while i < 16 { nm[i] = nm[i] / scl; i = i + 1 } 110 // power iteration 111 let v: *i64 = sys_mmap(32) as *i64 112 let w: *i64 = sys_mmap(32) as *i64 113 v[0] = RG_Q; v[1] = 0; v[2] = 0; v[3] = 0 // init toward identity quat 114 var it: i64 = 0 115 while it < 80 { 116 var a: i64 = 0 117 while a < 4 { 118 w[a] = (nm[a * 4] * v[0] + nm[a * 4 + 1] * v[1] + nm[a * 4 + 2] * v[2] + nm[a * 4 + 3] * v[3]) / RG_Q 119 a = a + 1 120 } 121 let len: i64 = rg_isqrt(w[0] * w[0] + w[1] * w[1] + w[2] * w[2] + w[3] * w[3]) 122 if len == 0 { v[0] = RG_Q; v[1] = 0; v[2] = 0; v[3] = 0 } else { 123 v[0] = (w[0] * RG_Q) / len; v[1] = (w[1] * RG_Q) / len; v[2] = (w[2] * RG_Q) / len; v[3] = (w[3] * RG_Q) / len 124 } 125 it = it + 1 126 } 127 // convention: keep w>=0 (quaternion double-cover) 128 if v[0] < 0 { v[0] = 0 - v[0]; v[1] = 0 - v[1]; v[2] = 0 - v[2]; v[3] = 0 - v[3] } 129 rg_quat_to_r(v, out_r) 130 // t = qcentroid - R * pcentroid 131 let rp: *i64 = sys_mmap(32) as *i64 132 rg_apply(out_r, 0, 0, 0, pcx, pcy, pcz, rp) 133 out_t[0] = qcx - rp[0] 134 out_t[1] = qcy - rp[1] 135 out_t[2] = qcz - rp[2] 136 return 0 137} 138 139// ============================================================================================ 140// ★★★SIMILARITY REGISTRATION -- rigid PLUS SCALE (7 DOF), added 2026-07-27 for the twin loop. 141// rg_register above solves R and t only, which is correct for fusing two scans OF THE SAME OBJECT at 142// the same size -- the cadtwin case it was written for. It is NOT enough to fit a PERSON: 143// * two bodies with identical proportions at different statures differ by a SCALE, and a rigid fit 144// cannot express that, so it reports a large residual for a subject who is merely a different size; 145// * uncalibrated photographs carry NO METRIC SCALE AT ALL, so scale is the one quantity you never 146// know going in. 147// Horn's own paper includes the scale factor; this implementation had dropped it. ★The symmetric form 148// s = sqrt(Sq/Sp) over the CENTERED clouds is used because it is INDEPENDENT OF THE ROTATION -- so 149// scale can be solved without trusting R, and a bad R cannot quietly corrupt s. 150// ⚠SCALE IS RECOVERED, NOT MEASURED. It tells you the ratio between two point sets. If one of them 151// came from an uncalibrated image, the resulting metric size is only as good as whatever real-world 152// length was used to anchor it -- that anchor is an ASSUMPTION and must be reported as one. 153// ⚠BOUNDS: Sp,Sq are sums of squared centered coords. At body scale (~2e3 mm, n<=1e4) Sq <= ~1.2e11, 154// and the largest intermediate below is Sq*RG_Q ~= 2e15 -- three orders clear of the i64 ceiling. 155func rg_centroid(p: *i64, n: i64, c3: *i64) -> i64 { 156 var cx: i64 = 0 157 var cy: i64 = 0 158 var cz: i64 = 0 159 var i: i64 = 0 160 while i < n { cx = cx + p[i*3]; cy = cy + p[i*3+1]; cz = cz + p[i*3+2]; i = i + 1 } 161 c3[0] = cx/n; c3[1] = cy/n; c3[2] = cz/n 162 return 0 163} 164// sum of squared distances from the centroid -- the cloud's "size", rotation-invariant by construction 165func rg_spread(p: *i64, n: i64) -> i64 { 166 let c: *i64 = sys_mmap(32) as *i64 167 rg_centroid(p, n, c) 168 var s: i64 = 0 169 var i: i64 = 0 170 while i < n { 171 let dx: i64 = p[i*3] - c[0] 172 let dy: i64 = p[i*3+1] - c[1] 173 let dz: i64 = p[i*3+2] - c[2] 174 s = s + dx*dx + dy*dy + dz*dz 175 i = i + 1 176 } 177 return s 178} 179// scale in Q14 such that Q ~= s*R*P + t. Degenerate (a cloud with no spread) returns unity rather 180// than dividing by zero -- a single point has no scale, and inventing one would be a lie. 181func rg_scale(p: *i64, q: *i64, n: i64) -> i64 { 182 let sp: i64 = rg_spread(p, n) 183 let sq: i64 = rg_spread(q, n) 184 if sp <= 0 { return RG_Q } 185 if sq <= 0 { return RG_Q } 186 let ratio: i64 = (sq * RG_Q) / sp 187 return rg_isqrt(ratio * RG_Q) 188} 189// apply s,R,t to a point 190func rg_apply_s(r: *i64, s: i64, tx: i64, ty: i64, tz: i64, px: i64, py: i64, pz: i64, out3: *i64) -> i64 { 191 let rx: i64 = (r[0]*px + r[1]*py + r[2]*pz) / RG_Q 192 let ry: i64 = (r[3]*px + r[4]*py + r[5]*pz) / RG_Q 193 let rz: i64 = (r[6]*px + r[7]*py + r[8]*pz) / RG_Q 194 out3[0] = (s*rx)/RG_Q + tx 195 out3[1] = (s*ry)/RG_Q + ty 196 out3[2] = (s*rz)/RG_Q + tz 197 return 0 198} 199// full similarity fit. out_s is Q14. Rotation reuses the PROVEN rg_register -- compose, do not 200// reimplement -- and only the scale and translation are new. 201// ★ARGUMENT ORDER IS (r, s, t), matching how the transform is WRITTEN: q = s*R*p + t. The first 202// version declared (r, t, s) and the gate called it (r, s, t) -- the mismatch returned the 203// TRANSLATION-X where the scale was expected, and because translation-x was 500 the wrong answer came 204// back as "502" instead of as garbage. ★LAW: three same-typed out-pointers in a row is a footgun; make 205// the order match the notation the caller already has in their head, rather than fixing the call site. 206func rg_register_sim(p: *i64, q: *i64, n: i64, out_r: *i64, out_s: *i64, out_t: *i64) -> i64 { 207 let rt: *i64 = sys_mmap(32) as *i64 208 rg_register(p, q, n, out_r, rt) 209 let s: i64 = rg_scale(p, q, n) 210 out_s[0] = s 211 let pc: *i64 = sys_mmap(32) as *i64 212 let qc: *i64 = sys_mmap(32) as *i64 213 rg_centroid(p, n, pc) 214 rg_centroid(q, n, qc) 215 let rp: *i64 = sys_mmap(32) as *i64 216 rg_apply_s(out_r, s, 0, 0, 0, pc[0], pc[1], pc[2], rp) 217 out_t[0] = qc[0] - rp[0] 218 out_t[1] = qc[1] - rp[1] 219 out_t[2] = qc[2] - rp[2] 220 return 0 221} 222// residual RMS under the similarity transform 223func rg_residual_s(p: *i64, q: *i64, n: i64, r: *i64, s: i64, t: *i64) -> i64 { 224 let o: *i64 = sys_mmap(32) as *i64 225 var sum: i64 = 0 226 var i: i64 = 0 227 while i < n { 228 rg_apply_s(r, s, t[0], t[1], t[2], p[i*3], p[i*3+1], p[i*3+2], o) 229 let dx: i64 = o[0] - q[i*3] 230 let dy: i64 = o[1] - q[i*3+1] 231 let dz: i64 = o[2] - q[i*3+2] 232 sum = sum + rg_isqrt(dx*dx + dy*dy + dz*dz) 233 i = i + 1 234 } 235 return sum / n 236} 237 238// residual RMS (coord): mean distance from (R*P+t) to Q over n corresponding points 239func rg_residual(p: *i64, q: *i64, n: i64, r: *i64, t: *i64) -> i64 { 240 let o: *i64 = sys_mmap(32) as *i64 241 var sum: i64 = 0 242 var i: i64 = 0 243 while i < n { 244 rg_apply(r, t[0], t[1], t[2], p[i * 3], p[i * 3 + 1], p[i * 3 + 2], o) 245 let dx: i64 = o[0] - q[i * 3] 246 let dy: i64 = o[1] - q[i * 3 + 1] 247 let dz: i64 = o[2] - q[i * 3 + 2] 248 sum = sum + rg_isqrt(dx * dx + dy * dy + dz * dz) 249 i = i + 1 250 } 251 return sum / n 252}