code wiki / (root) / nx_geom.nx

nx_geom.nx source

↩ module page · 405 lines · 13114 B

1// nx_geom.nx -- geometric primitives (V4 of vision rollout). 2// 3// Hough transform line detection (Hough 1962, Duda+Hart 1972) and 4// circle detection (Ballard 1981 gradient method). Pure i64. 5// 6// What this unlocks: 7// + room geometry (walls, doors, window frames as line peaks) 8// + architectural analysis (vanishing points via line intersection) 9// + vehicle/animal anatomy (wheels, eyes as circles) 10// + table-top objects (plates, cups, bottles as circles) 11// + rule-of-thirds + leading-line aesthetics (line-angle histogram) 12// 13// All trig is Q10 fixed-point. cos/sin tables hardcoded for 36 angles 14// (5 degree resolution) -- sufficient for major-line detection; 15// finer-grained vision would interpolate or use larger tables. 16// 17// genealogy_id: hough_1962_patent + duda_hart_1972_generalized 18// + ballard_1981_generalized_circle 19// lineage_id: voting_accumulator + gradient_direction_voting 20// axioms: NX_AX_GEOM_LINE_HOMOGENEOUS (rho = x cos t + y sin t) 21 22// nx_safety_envelope: 23// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 24// sil_target: SIL1 25// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 26// verdict: NOT_YET_EVALUATED 27 28import "syscalls.nx" 29import "nx_axioms.nx" 30import "nx_image.nx" 31import "nx_math.nx" 32 33// 36 angle bins at 5 degree resolution. 34const NX_GEOM_NTHETAS: i64 = 36 35const NX_GEOM_Q: i64 = 1024 36 37// Integer sqrt is canonical in nx_math.nx (Newton's method). 38 39// ===== Trig tables (Q10) ================================================ 40// 41// cos_t[i] = round(1024 * cos(i * 5 degrees)) for i in 0..35 42// sin_t[i] = round(1024 * sin(i * 5 degrees)) for i in 0..35 43// 44// These are recomputed per call (cheap, 36 stores) to keep the 45// primitive stateless. 46 47func nx_geom_fill_trig(cos_t: *i64, sin_t: *i64) -> i64 { 48 cos_t[0] = 1024 49 cos_t[1] = 1020 50 cos_t[2] = 1008 51 cos_t[3] = 989 52 cos_t[4] = 962 53 cos_t[5] = 928 54 cos_t[6] = 887 55 cos_t[7] = 839 56 cos_t[8] = 784 57 cos_t[9] = 724 58 cos_t[10] = 658 59 cos_t[11] = 587 60 cos_t[12] = 512 61 cos_t[13] = 433 62 cos_t[14] = 350 63 cos_t[15] = 265 64 cos_t[16] = 178 65 cos_t[17] = 89 66 cos_t[18] = 0 67 cos_t[19] = -89 68 cos_t[20] = -178 69 cos_t[21] = -265 70 cos_t[22] = -350 71 cos_t[23] = -433 72 cos_t[24] = -512 73 cos_t[25] = -587 74 cos_t[26] = -658 75 cos_t[27] = -724 76 cos_t[28] = -784 77 cos_t[29] = -839 78 cos_t[30] = -887 79 cos_t[31] = -928 80 cos_t[32] = -962 81 cos_t[33] = -989 82 cos_t[34] = -1008 83 cos_t[35] = -1020 84 85 sin_t[0] = 0 86 sin_t[1] = 89 87 sin_t[2] = 178 88 sin_t[3] = 265 89 sin_t[4] = 350 90 sin_t[5] = 433 91 sin_t[6] = 512 92 sin_t[7] = 587 93 sin_t[8] = 658 94 sin_t[9] = 724 95 sin_t[10] = 784 96 sin_t[11] = 839 97 sin_t[12] = 887 98 sin_t[13] = 928 99 sin_t[14] = 962 100 sin_t[15] = 989 101 sin_t[16] = 1008 102 sin_t[17] = 1020 103 sin_t[18] = 1024 104 sin_t[19] = 1020 105 sin_t[20] = 1008 106 sin_t[21] = 989 107 sin_t[22] = 962 108 sin_t[23] = 928 109 sin_t[24] = 887 110 sin_t[25] = 839 111 sin_t[26] = 784 112 sin_t[27] = 724 113 sin_t[28] = 658 114 sin_t[29] = 587 115 sin_t[30] = 512 116 sin_t[31] = 433 117 sin_t[32] = 350 118 sin_t[33] = 265 119 sin_t[34] = 178 120 sin_t[35] = 89 121 return 0 122} 123 124// ===== Hough line transform ============================================= 125// 126// Polar parametrization: rho = x*cos(theta) + y*sin(theta). 127// For theta in [0, 180) and rho in [-diag, +diag], each line maps to 128// a unique (rho, theta) point. Each edge pixel votes for all (theta, 129// rho) pairs consistent with the polar line equation. 130// 131// Returns up to max_lines lines satisfying votes >= min_votes and 132// being a 3x3 local maximum in the accumulator. 133 134struct HoughLines { 135 n_lines: i64, 136 rhos: *i64, // length n_lines 137 thetas: *i64, // degrees (0..175 step 5) 138 votes: *i64, 139} 140 141func nx_geom_hough_line(edge_mask: *Image, min_votes: i64, 142 max_lines: i64) -> *HoughLines { 143 let cos_t: *i64 = (sys_mmap(36 * 8)) as *i64 144 let sin_t: *i64 = (sys_mmap(36 * 8)) as *i64 145 nx_geom_fill_trig(cos_t, sin_t) 146 147 let w: i64 = edge_mask.width 148 let h: i64 = edge_mask.height 149 let d2: i64 = w * w + h * h 150 var diag: i64 = nx_math_isqrt(d2) + 1 151 let n_rhos: i64 = 2 * diag + 1 152 let acc_size: i64 = 36 * n_rhos 153 let acc: *i64 = (sys_mmap(acc_size * 8)) as *i64 154 var zi: i64 = 0 155 while zi < acc_size { 156 acc[zi] = 0 157 zi = zi + 1 158 } 159 160 // Accumulate votes. 161 var y: i64 = 0 162 while y < h { 163 var x: i64 = 0 164 while x < w { 165 if nx_image_get(edge_mask, x, y, 0) > 0 { 166 var t: i64 = 0 167 while t < 36 { 168 let rho_q: i64 = x * cos_t[t] + y * sin_t[t] 169 var rho: i64 = rho_q / 1024 170 let idx: i64 = t * n_rhos + (rho + diag) 171 if idx >= 0 { 172 if idx < acc_size { 173 acc[idx] = acc[idx] + 1 174 } 175 } 176 t = t + 1 177 } 178 } 179 x = x + 1 180 } 181 y = y + 1 182 } 183 184 let lines: *HoughLines = (sys_mmap(32)) as *HoughLines 185 lines.rhos = (sys_mmap(max_lines * 8)) as *i64 186 lines.thetas = (sys_mmap(max_lines * 8)) as *i64 187 lines.votes = (sys_mmap(max_lines * 8)) as *i64 188 lines.n_lines = 0 189 190 // Find peaks (3x3 local maximum, >= min_votes). 191 var t2: i64 = 0 192 while t2 < 36 { 193 var ri: i64 = 0 194 while ri < n_rhos { 195 let v: i64 = acc[t2 * n_rhos + ri] 196 if v >= min_votes { 197 var is_peak: i64 = 1 198 var dt: i64 = -1 199 while dt <= 1 { 200 var dr: i64 = -1 201 while dr <= 1 { 202 var is_center: i64 = 0 203 if dt == 0 { if dr == 0 { is_center = 1 } } 204 if is_center == 0 { 205 let nt: i64 = t2 + dt 206 let nr: i64 = ri + dr 207 if nt >= 0 { 208 if nt < 36 { 209 if nr >= 0 { 210 if nr < n_rhos { 211 if acc[nt * n_rhos + nr] > v { 212 is_peak = 0 213 } 214 } 215 } 216 } 217 } 218 } 219 dr = dr + 1 220 } 221 dt = dt + 1 222 } 223 if is_peak == 1 { 224 if lines.n_lines < max_lines { 225 lines.thetas[lines.n_lines] = t2 * 5 226 lines.rhos[lines.n_lines] = ri - diag 227 lines.votes[lines.n_lines] = v 228 lines.n_lines = lines.n_lines + 1 229 } 230 } 231 } 232 ri = ri + 1 233 } 234 t2 = t2 + 1 235 } 236 return lines 237} 238 239// ===== Hough circle (fixed radius, gradient-direction voting) =========== 240// 241// Ballard 1981 trick: an edge pixel at (x, y) with gradient (gx, gy) 242// lies on a circle of radius r whose center is at distance r along 243// the gradient direction (or opposite). This reduces the 3D Hough 244// accumulator to 2D, giving huge speedup. 245// 246// Inputs: 247// edge: binary mask 248// gx, gy: gradient images (from nx_image_sobel_x/y) 249// r: known radius to search for 250// min_votes: peak threshold 251// max_circles: cap on returned circles 252// 253// Returns up to max_circles centers (cx, cy) at radius r. 254 255struct HoughCircles { 256 n_circles: i64, 257 cxs: *i64, 258 cys: *i64, 259 rs: *i64, 260 votes: *i64, 261} 262 263func nx_geom_hough_circle_r(edge: *Image, gx: *ImageS64, gy: *ImageS64, 264 r: i64, min_votes: i64, 265 max_circles: i64) -> *HoughCircles { 266 let w: i64 = edge.width 267 let h: i64 = edge.height 268 let acc_n: i64 = w * h 269 let acc: *i64 = (sys_mmap(acc_n * 8)) as *i64 270 var zi: i64 = 0 271 while zi < acc_n { 272 acc[zi] = 0 273 zi = zi + 1 274 } 275 276 // Vote for centers along the gradient direction. 277 var y: i64 = 0 278 while y < h { 279 var x: i64 = 0 280 while x < w { 281 if nx_image_get(edge, x, y, 0) > 0 { 282 let gxv: i64 = nx_image_s64_get(gx, x, y) 283 let gyv: i64 = nx_image_s64_get(gy, x, y) 284 let mag2: i64 = gxv * gxv + gyv * gyv 285 if mag2 > 0 { 286 let mag: i64 = nx_math_isqrt(mag2) 287 if mag > 0 { 288 let dx: i64 = (r * gxv) / mag 289 let dy: i64 = (r * gyv) / mag 290 let cx1: i64 = x + dx 291 let cy1: i64 = y + dy 292 let cx2: i64 = x - dx 293 let cy2: i64 = y - dy 294 if cx1 >= 0 { 295 if cx1 < w { 296 if cy1 >= 0 { 297 if cy1 < h { 298 let i1: i64 = cy1 * w + cx1 299 acc[i1] = acc[i1] + 1 300 } 301 } 302 } 303 } 304 if cx2 >= 0 { 305 if cx2 < w { 306 if cy2 >= 0 { 307 if cy2 < h { 308 let i2: i64 = cy2 * w + cx2 309 acc[i2] = acc[i2] + 1 310 } 311 } 312 } 313 } 314 } 315 } 316 } 317 x = x + 1 318 } 319 y = y + 1 320 } 321 322 let circles: *HoughCircles = (sys_mmap(40)) as *HoughCircles 323 circles.cxs = (sys_mmap(max_circles * 8)) as *i64 324 circles.cys = (sys_mmap(max_circles * 8)) as *i64 325 circles.rs = (sys_mmap(max_circles * 8)) as *i64 326 circles.votes = (sys_mmap(max_circles * 8)) as *i64 327 circles.n_circles = 0 328 329 var py: i64 = 0 330 while py < h { 331 var px: i64 = 0 332 while px < w { 333 let v: i64 = acc[py * w + px] 334 if v >= min_votes { 335 var is_peak: i64 = 1 336 var dy2: i64 = -1 337 while dy2 <= 1 { 338 var dx2: i64 = -1 339 while dx2 <= 1 { 340 var is_center: i64 = 0 341 if dx2 == 0 { if dy2 == 0 { is_center = 1 } } 342 if is_center == 0 { 343 let nx2: i64 = px + dx2 344 let ny2: i64 = py + dy2 345 if nx2 >= 0 { 346 if nx2 < w { 347 if ny2 >= 0 { 348 if ny2 < h { 349 if acc[ny2 * w + nx2] > v { 350 is_peak = 0 351 } 352 } 353 } 354 } 355 } 356 } 357 dx2 = dx2 + 1 358 } 359 dy2 = dy2 + 1 360 } 361 if is_peak == 1 { 362 if circles.n_circles < max_circles { 363 circles.cxs[circles.n_circles] = px 364 circles.cys[circles.n_circles] = py 365 circles.rs[circles.n_circles] = r 366 circles.votes[circles.n_circles] = v 367 circles.n_circles = circles.n_circles + 1 368 } 369 } 370 } 371 px = px + 1 372 } 373 py = py + 1 374 } 375 return circles 376} 377 378// ===== Line-orientation histogram ======================================= 379// 380// Aggregate Hough line votes by orientation bin. Useful for: 381// - room detection (peaks at 0 and 90 degrees -> rectilinear room) 382// - diagonal composition (peak at 45 -> dynamic composition) 383// - radial composition (broad distribution -> radial layout) 384// 385// Returns histogram[36] of total votes per angle bin. 386 387func nx_geom_line_orientation_hist(lines: *HoughLines, hist: *i64) -> i64 { 388 var i: i64 = 0 389 while i < 36 { 390 hist[i] = 0 391 i = i + 1 392 } 393 var li: i64 = 0 394 while li < lines.n_lines { 395 let theta: i64 = lines.thetas[li] 396 let bin: i64 = theta / 5 397 if bin >= 0 { 398 if bin < 36 { 399 hist[bin] = hist[bin] + lines.votes[li] 400 } 401 } 402 li = li + 1 403 } 404 return 0 405}