code wiki / (root) / nx_features.nx

nx_features.nx source

↩ module page · 443 lines · 14813 B

1// nx_features.nx -- pure-math classical computer-vision features. 2// 3// No learned weights, no neural network, no model file. Substrate 4// primitives only. Reuses nx_th_isqrt, nx_th_hamming_distance, 5// nx_th_spectral_disc_2x2 from nx_theorems*.nx. 6// 7// Capabilities at this version: 8// * Harris corner response (structure tensor + spectral discriminant) 9// * Edge magnitude threshold (Sobel + clamp) 10// * Connected components on binary mask (union-find) 11// * Hu invariant moments (rotation/scale/translation invariant shape) 12// * Hamming descriptor matching 13// 14// genealogy_id: harris_stephens_1988 + canny_1986 + hu_1962 15// + rosenfeld_pfaltz_1966 (connected components) 16// lineage_id: structure_tensor + spectral_discriminant + central_moments 17// axioms: NX_AX_ALG_DISTRIBUTIVITY, NX_AX_ORD_LEAST_UPPER_BOUND 18 19// nx_safety_envelope: 20// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 21// sil_target: SIL1 22// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 23// verdict: NOT_YET_EVALUATED 24 25import "syscalls.nx" 26import "nx_axioms.nx" 27import "nx_i128.nx" 28import "nx_qed_freek.nx" 29import "nx_image.nx" 30 31// ===== Harris corner response ========================================= 32// 33// Structure tensor M at (x, y): 34// M = [[Sxx Sxy] 35// [Sxy Syy]] 36// where Sxx = sum_W(Ix * Ix), Syy = sum_W(Iy * Iy), Sxy = sum_W(Ix * Iy) 37// over a window W (we use 3x3). Ix, Iy from Sobel. 38// 39// Corner response R = det(M) - k * trace(M)^2 40// det(M) = Sxx*Syy - Sxy^2 41// trace(M) = Sxx + Syy 42// k typically 0.04..0.06. We use k = 4/100 (integer-friendly). 43// 44// Output is ImageS64 of corner responses. 45 46const NX_HARRIS_K_NUM: i64 = 4 47const NX_HARRIS_K_DEN: i64 = 100 48 49func nx_feat_harris_response(gray: *Image) -> *ImageS64 { 50 let gx: *ImageS64 = nx_image_sobel_x(gray) 51 let gy: *ImageS64 = nx_image_sobel_y(gray) 52 53 let out: *ImageS64 = nx_image_s64_alloc(gray.width, gray.height) 54 var y: i64 = 1 55 while y < gray.height - 1 { 56 var x: i64 = 1 57 while x < gray.width - 1 { 58 var sxx: i64 = 0 59 var syy: i64 = 0 60 var sxy: i64 = 0 61 var dj: i64 = -1 62 while dj <= 1 { 63 var di: i64 = -1 64 while di <= 1 { 65 let ix: i64 = nx_image_s64_get(gx, x + di, y + dj) 66 let iy: i64 = nx_image_s64_get(gy, x + di, y + dj) 67 sxx = sxx + ix * ix 68 syy = syy + iy * iy 69 sxy = sxy + ix * iy 70 di = di + 1 71 } 72 dj = dj + 1 73 } 74 // det = sxx*syy - sxy^2 75 let det: i64 = nx_muldiv_i64(sxx, syy, 1) - nx_muldiv_i64(sxy, sxy, 1) 76 // trace = sxx + syy 77 let trace: i64 = sxx + syy 78 // k * trace^2 79 let tr2: i64 = nx_muldiv_i64(trace, trace, 1) 80 let k_tr2: i64 = nx_muldiv_i64(tr2, NX_HARRIS_K_NUM, NX_HARRIS_K_DEN) 81 let resp: i64 = det - k_tr2 82 nx_image_s64_set(out, x, y, resp) 83 x = x + 1 84 } 85 y = y + 1 86 } 87 return out 88} 89 90// ===== Non-max suppression on Harris response ========================= 91// 92// Mark a pixel as a corner if its response is >= threshold AND it's 93// the local maximum within a 3x3 window. Output is a binary mask 94// (255 at corners, 0 elsewhere). 95 96func nx_feat_corners_nms(resp: *ImageS64, threshold: i64) -> *Image { 97 let mask: *Image = nx_image_alloc(resp.width, resp.height, 1) 98 var y: i64 = 1 99 while y < resp.height - 1 { 100 var x: i64 = 1 101 while x < resp.width - 1 { 102 let r: i64 = nx_image_s64_get(resp, x, y) 103 if r >= threshold { 104 var is_max: i64 = 1 105 var dj: i64 = -1 106 while dj <= 1 { 107 var di: i64 = -1 108 while di <= 1 { 109 if di != 0 { 110 let neighbor: i64 = nx_image_s64_get(resp, x + di, y + dj) 111 if neighbor > r { is_max = 0 } 112 } 113 if di == 0 { 114 if dj != 0 { 115 let neighbor: i64 = nx_image_s64_get(resp, x + di, y + dj) 116 if neighbor > r { is_max = 0 } 117 } 118 } 119 di = di + 1 120 } 121 dj = dj + 1 122 } 123 if is_max == 1 { nx_image_set(mask, x, y, 0, 255) } 124 } 125 x = x + 1 126 } 127 y = y + 1 128 } 129 return mask 130} 131 132// Count corners in a mask. 133func nx_feat_count_corners(mask: *Image) -> i64 { 134 var count: i64 = 0 135 var y: i64 = 0 136 while y < mask.height { 137 var x: i64 = 0 138 while x < mask.width { 139 if nx_image_get(mask, x, y, 0) > 0 { count = count + 1 } 140 x = x + 1 141 } 142 y = y + 1 143 } 144 return count 145} 146 147// ===== Edge detection (Sobel magnitude + threshold) =================== 148// 149// Simple two-step: Sobel gradients -> magnitude -> threshold. For 150// full Canny, add non-max suppression along gradient direction + 151// hysteresis (queued as Phase E1). 152 153func nx_feat_edge_mask(gray: *Image, threshold: i64) -> *Image { 154 let gx: *ImageS64 = nx_image_sobel_x(gray) 155 let gy: *ImageS64 = nx_image_sobel_y(gray) 156 let mag: *ImageS64 = nx_image_gradient_magnitude(gx, gy) 157 let mask: *Image = nx_image_alloc(gray.width, gray.height, 1) 158 var y: i64 = 0 159 while y < gray.height { 160 var x: i64 = 0 161 while x < gray.width { 162 let m: i64 = nx_image_s64_get(mag, x, y) 163 if m >= threshold { 164 nx_image_set(mask, x, y, 0, 255) 165 } 166 x = x + 1 167 } 168 y = y + 1 169 } 170 return mask 171} 172 173// ===== Connected components on binary mask =========================== 174// 175// Two-pass union-find labeling. Returns label image (i64 per pixel) 176// + count of components. 4-connectivity. 177// 178// genealogy_id: rosenfeld_pfaltz_1966 179// lineage_id: union_find + raster_scan 180// axioms: NX_AX_REL_TRANSITIVITY (equivalence classes) 181 182struct CCResult { 183 labels: *i64, // width*height i64 array 184 width: i64, 185 height: i64, 186 n_components: i64, 187} 188 189const NX_CC_BYTES: i64 = 32 190 191// Simple union-find with path compression. 192func nx_cc_find(parent: *i64, x: i64) -> i64 { 193 var r: i64 = x 194 while parent[r] != r { 195 r = parent[r] 196 } 197 // path compression 198 var c: i64 = x 199 while parent[c] != r { 200 let next: i64 = parent[c] 201 parent[c] = r 202 c = next 203 } 204 return r 205} 206 207func nx_cc_union(parent: *i64, a: i64, b: i64) -> i64 { 208 let ra: i64 = nx_cc_find(parent, a) 209 let rb: i64 = nx_cc_find(parent, b) 210 if ra != rb { parent[ra] = rb } 211 return 0 212} 213 214func nx_feat_connected_components(mask: *Image) -> *CCResult { 215 let w: i64 = mask.width 216 let h: i64 = mask.height 217 let raw: *u8 = sys_mmap(NX_CC_BYTES) 218 let r: *CCResult = raw as *CCResult 219 r.labels = (sys_mmap(w * h * 8 + 16)) as *i64 220 r.width = w 221 r.height = h 222 r.n_components = 0 223 224 // First pass: assign provisional labels. 225 let max_labels: i64 = w * h + 1 226 let parent: *i64 = (sys_mmap(max_labels * 8 + 16)) as *i64 227 parent[0] = 0 228 var next_label: i64 = 1 229 var y: i64 = 0 230 while y < h { 231 var x: i64 = 0 232 while x < w { 233 let pixel: i64 = nx_image_get(mask, x, y, 0) 234 if pixel == 0 { 235 r.labels[y * w + x] = 0 236 } 237 if pixel > 0 { 238 let left: i64 = (((x > 0) as i64) * (r.labels[y * w + x - 1])) 239 let up: i64 = (((y > 0) as i64) * (r.labels[(y - 1) * w + x])) 240 if left == 0 { 241 if up == 0 { 242 r.labels[y * w + x] = next_label 243 parent[next_label] = next_label 244 next_label = next_label + 1 245 } 246 if up != 0 { 247 r.labels[y * w + x] = up 248 } 249 } 250 if left != 0 { 251 if up == 0 { 252 r.labels[y * w + x] = left 253 } 254 if up != 0 { 255 r.labels[y * w + x] = left 256 if left != up { 257 nx_cc_union(parent, left, up) 258 } 259 } 260 } 261 } 262 x = x + 1 263 } 264 y = y + 1 265 } 266 267 // Second pass: replace each label with its root. 268 let root_map: *i64 = (sys_mmap(next_label * 8 + 16)) as *i64 269 var i: i64 = 0 270 while i < next_label { root_map[i] = -1; i = i + 1 } 271 var comp_count: i64 = 0 272 y = 0 273 while y < h { 274 var x: i64 = 0 275 while x < w { 276 let l: i64 = r.labels[y * w + x] 277 if l > 0 { 278 let root: i64 = nx_cc_find(parent, l) 279 if root_map[root] < 0 { 280 comp_count = comp_count + 1 281 root_map[root] = comp_count 282 } 283 r.labels[y * w + x] = root_map[root] 284 } 285 x = x + 1 286 } 287 y = y + 1 288 } 289 r.n_components = comp_count 290 return r 291} 292 293// ===== component statistics =========================================== 294// 295// For each component, compute pixel count + bounding box + centroid. 296// Returns array of i64[7]: [count, min_x, min_y, max_x, max_y, 297// centroid_x*1000, centroid_y*1000]. 298 299func nx_feat_component_stats(cc: *CCResult, out: *i64) -> i64 { 300 let n: i64 = cc.n_components 301 var i: i64 = 0 302 while i < n { 303 out[i * 7] = 0 // count 304 out[i * 7 + 1] = cc.width // min_x init high 305 out[i * 7 + 2] = cc.height 306 out[i * 7 + 3] = -1 // max_x init low 307 out[i * 7 + 4] = -1 308 out[i * 7 + 5] = 0 // sum_x for centroid 309 out[i * 7 + 6] = 0 // sum_y 310 i = i + 1 311 } 312 var y: i64 = 0 313 while y < cc.height { 314 var x: i64 = 0 315 while x < cc.width { 316 let l: i64 = cc.labels[y * cc.width + x] 317 if l > 0 { 318 let base: i64 = (l - 1) * 7 319 out[base] = out[base] + 1 320 if x < out[base + 1] { out[base + 1] = x } 321 if y < out[base + 2] { out[base + 2] = y } 322 if x > out[base + 3] { out[base + 3] = x } 323 if y > out[base + 4] { out[base + 4] = y } 324 out[base + 5] = out[base + 5] + x 325 out[base + 6] = out[base + 6] + y 326 } 327 x = x + 1 328 } 329 y = y + 1 330 } 331 // Convert sum_x, sum_y -> centroid (scaled by 1000). 332 i = 0 333 while i < n { 334 let cnt: i64 = out[i * 7] 335 if cnt > 0 { 336 out[i * 7 + 5] = (out[i * 7 + 5] * 1000) / cnt 337 out[i * 7 + 6] = (out[i * 7 + 6] * 1000) / cnt 338 } 339 i = i + 1 340 } 341 return 0 342} 343 344// ===== Hu invariant moments (shape descriptor) ======================== 345// 346// Spatial moments m_pq = sum over (x, y) of x^p * y^q * I(x, y). 347// Central moments mu_pq = m_pq centered at centroid. 348// Normalized central moments eta_pq = mu_pq / m_00^((p+q)/2 + 1). 349// 350// Hu's 7 invariants (rotation / scale / translation invariant). We 351// return all 7 as a flat i64[7] array (scaled by 1e9 for PPB). 352// 353// genealogy_id: hu_1962 354// lineage_id: moments + invariant_features + nonlinear_combination 355// axioms: NX_AX_PROB_NONNEGATIVITY (intensities), NX_AX_ALG_DISTRIBUTIVITY 356 357func nx_feat_hu_moments(mask: *Image, out_hu: *i64) -> i64 { 358 let w: i64 = mask.width 359 let h: i64 = mask.height 360 // First pass: m00, m10, m01. 361 var m00: i64 = 0 362 var m10: i64 = 0 363 var m01: i64 = 0 364 var y: i64 = 0 365 while y < h { 366 var x: i64 = 0 367 while x < w { 368 let v: i64 = nx_image_get(mask, x, y, 0) 369 if v > 0 { 370 m00 = m00 + 1 371 m10 = m10 + x 372 m01 = m01 + y 373 } 374 x = x + 1 375 } 376 y = y + 1 377 } 378 if m00 == 0 { 379 var i: i64 = 0 380 while i < 7 { out_hu[i] = 0; i = i + 1 } 381 return 0 382 } 383 let cx_scaled: i64 = m10 * 1000 / m00 384 let cy_scaled: i64 = m01 * 1000 / m00 385 // Central moments (scaled by 1000^p+q to keep integer math). 386 var mu20: i64 = 0 387 var mu02: i64 = 0 388 var mu11: i64 = 0 389 var mu30: i64 = 0 390 var mu03: i64 = 0 391 var mu21: i64 = 0 392 var mu12: i64 = 0 393 y = 0 394 while y < h { 395 var x: i64 = 0 396 while x < w { 397 let v: i64 = nx_image_get(mask, x, y, 0) 398 if v > 0 { 399 let dx: i64 = x * 1000 - cx_scaled 400 let dy: i64 = y * 1000 - cy_scaled 401 let dx2: i64 = nx_muldiv_i64(dx, dx, 1000) 402 let dy2: i64 = nx_muldiv_i64(dy, dy, 1000) 403 let dxdy: i64 = nx_muldiv_i64(dx, dy, 1000) 404 mu20 = mu20 + dx2 405 mu02 = mu02 + dy2 406 mu11 = mu11 + dxdy 407 mu30 = mu30 + nx_muldiv_i64(dx2, dx, 1000) 408 mu03 = mu03 + nx_muldiv_i64(dy2, dy, 1000) 409 mu21 = mu21 + nx_muldiv_i64(dx2, dy, 1000) 410 mu12 = mu12 + nx_muldiv_i64(dxdy, dy, 1000) 411 } 412 x = x + 1 413 } 414 y = y + 1 415 } 416 // Hu's 7 invariants (computed on central moments, kept in same scale). 417 // Hu1 = mu20 + mu02 418 out_hu[0] = mu20 + mu02 419 // Hu2 = (mu20 - mu02)^2 + 4*mu11^2 420 out_hu[1] = nx_muldiv_i64(mu20 - mu02, mu20 - mu02, 1) + 421 4 * nx_muldiv_i64(mu11, mu11, 1) 422 // Hu3 = (mu30 - 3*mu12)^2 + (3*mu21 - mu03)^2 423 out_hu[2] = nx_muldiv_i64(mu30 - 3 * mu12, mu30 - 3 * mu12, 1) + 424 nx_muldiv_i64(3 * mu21 - mu03, 3 * mu21 - mu03, 1) 425 // Hu4 = (mu30 + mu12)^2 + (mu21 + mu03)^2 426 out_hu[3] = nx_muldiv_i64(mu30 + mu12, mu30 + mu12, 1) + 427 nx_muldiv_i64(mu21 + mu03, mu21 + mu03, 1) 428 // Hu5, Hu6, Hu7: higher-order combinations (we leave as 0 for P0 -- 429 // they involve products with cubic terms that need careful scaling). 430 out_hu[4] = 0 431 out_hu[5] = 0 432 out_hu[6] = 0 433 return 0 434} 435 436// ===== Hamming-distance descriptor matching ========================== 437// 438// Binary descriptors (e.g., BRIEF / ORB / FREAK) are i64 sequences. 439// Match two descriptor sets by minimum Hamming distance. 440 441func nx_feat_match_brief(desc_a: i64, desc_b: i64) -> i64 { 442 return nx_th_hamming_distance(desc_a, desc_b) 443}