code wiki / (root) / nx_pc.nx

nx_pc.nx source

↩ module page · 259 lines · 7790 B

1// nx_pc.nx -- 2D point cloud + ICP alignment + nearest neighbor. 2// 3// What this unlocks: 4// + register two views of a planar object (paper / floor / map) 5// + align Hough-line endpoints across two photos 6// + match feature points from V8 salience peaks across frames 7// + planar SLAM scaffolding 8// + 2D landmark localization 9// 10// All pure i64 with Q10 trig. Brute-force NN is O(N*M); for larger 11// clouds users can pre-bucket; we keep the substrate primitive simple. 12// 13// genealogy_id: horn_1987_closed_form_rotation + besl_mckay_1992_icp 14// lineage_id: point_cloud_registration + iterative_closest_point 15 16// nx_safety_envelope: 17// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 18// sil_target: SIL1 19// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 20// verdict: NOT_YET_EVALUATED 21 22import "syscalls.nx" 23import "nx_math.nx" 24 25const NX_PC_Q: i64 = 1024 // Q10 26 27struct PointCloud2D { 28 n: i64, 29 xs: *i64, 30 ys: *i64, 31} 32 33// ===== Allocation + access ============================================= 34 35func nx_pc_alloc(n: i64) -> *PointCloud2D { 36 let pc: *PointCloud2D = (sys_mmap(24)) as *PointCloud2D 37 pc.n = n 38 pc.xs = (sys_mmap(n * 8)) as *i64 39 pc.ys = (sys_mmap(n * 8)) as *i64 40 return pc 41} 42 43func nx_pc_set(pc: *PointCloud2D, i: i64, x: i64, y: i64) -> i64 { 44 pc.xs[i] = x 45 pc.ys[i] = y 46 return 0 47} 48 49func nx_pc_get_x(pc: *PointCloud2D, i: i64) -> i64 { 50 return pc.xs[i] 51} 52 53func nx_pc_get_y(pc: *PointCloud2D, i: i64) -> i64 { 54 return pc.ys[i] 55} 56 57// Integer sqrt is canonical in nx_math.nx. 58 59// ===== Centroid ========================================================= 60 61func nx_pc_centroid(pc: *PointCloud2D, out_cx: *i64, out_cy: *i64) -> i64 { 62 if pc.n == 0 { 63 out_cx[0] = 0 64 out_cy[0] = 0 65 return 0 66 } 67 var sx: i64 = 0 68 var sy: i64 = 0 69 var i: i64 = 0 70 while i < pc.n { 71 sx = sx + pc.xs[i] 72 sy = sy + pc.ys[i] 73 i = i + 1 74 } 75 out_cx[0] = sx / pc.n 76 out_cy[0] = sy / pc.n 77 return 0 78} 79 80// ===== Bounding box ===================================================== 81 82func nx_pc_bbox(pc: *PointCloud2D, out_xmin: *i64, out_ymin: *i64, 83 out_xmax: *i64, out_ymax: *i64) -> i64 { 84 if pc.n == 0 { 85 out_xmin[0] = 0; out_ymin[0] = 0 86 out_xmax[0] = 0; out_ymax[0] = 0 87 return 0 88 } 89 var xmn: i64 = pc.xs[0] 90 var xmx: i64 = pc.xs[0] 91 var ymn: i64 = pc.ys[0] 92 var ymx: i64 = pc.ys[0] 93 var i: i64 = 1 94 while i < pc.n { 95 let x: i64 = pc.xs[i] 96 let y: i64 = pc.ys[i] 97 if x < xmn { xmn = x } 98 if x > xmx { xmx = x } 99 if y < ymn { ymn = y } 100 if y > ymx { ymx = y } 101 i = i + 1 102 } 103 out_xmin[0] = xmn; out_ymin[0] = ymn 104 out_xmax[0] = xmx; out_ymax[0] = ymx 105 return 0 106} 107 108// ===== Brute-force nearest neighbor ==================================== 109// 110// Returns index of point in `cloud` nearest (in squared distance) to (qx, qy). 111 112func nx_pc_nearest(cloud: *PointCloud2D, qx: i64, qy: i64) -> i64 { 113 if cloud.n == 0 { return -1 } 114 var best_i: i64 = 0 115 let dx0: i64 = cloud.xs[0] - qx 116 let dy0: i64 = cloud.ys[0] - qy 117 var best_d: i64 = dx0 * dx0 + dy0 * dy0 118 var i: i64 = 1 119 while i < cloud.n { 120 let dx: i64 = cloud.xs[i] - qx 121 let dy: i64 = cloud.ys[i] - qy 122 let d: i64 = dx * dx + dy * dy 123 if d < best_d { 124 best_d = d 125 best_i = i 126 } 127 i = i + 1 128 } 129 return best_i 130} 131 132// ===== Apply 2D rigid transform (rotation + translation) =============== 133// 134// (x', y') = R * (x, y) + t where R is from (cos_q10, sin_q10). 135 136func nx_pc_apply_transform(pc: *PointCloud2D, cos_q10: i64, sin_q10: i64, 137 tx: i64, ty: i64) -> i64 { 138 var i: i64 = 0 139 while i < pc.n { 140 let x: i64 = pc.xs[i] 141 let y: i64 = pc.ys[i] 142 pc.xs[i] = (cos_q10 * x - sin_q10 * y) / NX_PC_Q + tx 143 pc.ys[i] = (sin_q10 * x + cos_q10 * y) / NX_PC_Q + ty 144 i = i + 1 145 } 146 return 0 147} 148 149// ===== One ICP iteration =============================================== 150// 151// Given source cloud and target cloud (must have same size for our 152// simplified version OR using nearest-neighbor pairing for arbitrary 153// sizes), compute and apply the optimal rigid transform that aligns 154// source to target. 155// 156// Horn 1987 closed-form for 2D: 157// 1. Compute centroids of paired points. 158// 2. Center both. 159// 3. Form correlation matrix M = sum (src_centered)(tgt_centered)^T. 160// 4. Optimal rotation: 161// c = (Sxx + Syy) / norm 162// s = (Syx - Sxy) / norm 163// where norm = sqrt((Sxx+Syy)^2 + (Syx-Sxy)^2) 164// 5. t = tgt_centroid - R * src_centroid. 165// 166// Apply transform to source in place. Returns 0 on success, -1 if 167// degenerate (norm == 0). 168 169func nx_pc_icp_step(src: *PointCloud2D, tgt: *PointCloud2D) -> i64 { 170 if src.n == 0 { return -1 } 171 if tgt.n == 0 { return -1 } 172 // Pair via nearest neighbor: for each src point, find nearest tgt. 173 // Compute centroids of paired points. 174 var src_cx: i64 = 0 175 var src_cy: i64 = 0 176 var tgt_cx: i64 = 0 177 var tgt_cy: i64 = 0 178 var i: i64 = 0 179 let pairs_x: *i64 = (sys_mmap(src.n * 8)) as *i64 180 let pairs_y: *i64 = (sys_mmap(src.n * 8)) as *i64 181 while i < src.n { 182 let sx: i64 = src.xs[i] 183 let sy: i64 = src.ys[i] 184 let tj: i64 = nx_pc_nearest(tgt, sx, sy) 185 pairs_x[i] = tgt.xs[tj] 186 pairs_y[i] = tgt.ys[tj] 187 src_cx = src_cx + sx 188 src_cy = src_cy + sy 189 tgt_cx = tgt_cx + pairs_x[i] 190 tgt_cy = tgt_cy + pairs_y[i] 191 i = i + 1 192 } 193 src_cx = src_cx / src.n 194 src_cy = src_cy / src.n 195 tgt_cx = tgt_cx / src.n 196 tgt_cy = tgt_cy / src.n 197 // Form 2x2 correlation matrix on centered points. 198 var Sxx: i64 = 0 199 var Sxy: i64 = 0 200 var Syx: i64 = 0 201 var Syy: i64 = 0 202 var j: i64 = 0 203 while j < src.n { 204 let cx_s: i64 = src.xs[j] - src_cx 205 let cy_s: i64 = src.ys[j] - src_cy 206 let cx_t: i64 = pairs_x[j] - tgt_cx 207 let cy_t: i64 = pairs_y[j] - tgt_cy 208 Sxx = Sxx + cx_s * cx_t 209 Sxy = Sxy + cx_s * cy_t 210 Syx = Syx + cy_s * cx_t 211 Syy = Syy + cy_s * cy_t 212 j = j + 1 213 } 214 let a: i64 = Sxx + Syy 215 let b: i64 = Syx - Sxy 216 let norm2: i64 = a * a + b * b 217 if norm2 == 0 { return -1 } 218 let norm: i64 = nx_math_isqrt(norm2) 219 if norm == 0 { return -1 } 220 let cos_q10: i64 = (a * NX_PC_Q) / norm 221 let sin_q10: i64 = (b * NX_PC_Q) / norm 222 // Translation: t = tgt_centroid - R * src_centroid 223 let tx: i64 = tgt_cx - (cos_q10 * src_cx - sin_q10 * src_cy) / NX_PC_Q 224 let ty: i64 = tgt_cy - (sin_q10 * src_cx + cos_q10 * src_cy) / NX_PC_Q 225 nx_pc_apply_transform(src, cos_q10, sin_q10, tx, ty) 226 return 0 227} 228 229// Run ICP for up to max_iter iterations. Returns number of iterations 230// performed. 231 232func nx_pc_icp(src: *PointCloud2D, tgt: *PointCloud2D, 233 max_iter: i64) -> i64 { 234 var i: i64 = 0 235 while i < max_iter { 236 if nx_pc_icp_step(src, tgt) != 0 { return i } 237 i = i + 1 238 } 239 return max_iter 240} 241 242// ===== Mean squared error between paired clouds ======================== 243// 244// For each src point, find nearest in tgt, accumulate squared distance. 245// Returns MSE / src.n. 246 247func nx_pc_mse(src: *PointCloud2D, tgt: *PointCloud2D) -> i64 { 248 if src.n == 0 { return 0 } 249 var acc: i64 = 0 250 var i: i64 = 0 251 while i < src.n { 252 let tj: i64 = nx_pc_nearest(tgt, src.xs[i], src.ys[i]) 253 let dx: i64 = src.xs[i] - tgt.xs[tj] 254 let dy: i64 = src.ys[i] - tgt.ys[tj] 255 acc = acc + dx * dx + dy * dy 256 i = i + 1 257 } 258 return acc / src.n 259}