code wiki / (root) / nx_landmark_anatomy.nx

nx_landmark_anatomy.nx source

↩ module page · 419 lines · 14929 B

1// nx_landmark_anatomy.nx -- heuristic anatomy-region labeler. 2// 3// CAPABILITY_COMPLETENESS: PARTIAL 4// MISSING_CAPABILITIES: 5// - face keypoint detection (eye/nose/mouth points within 6// head bbox): queued; needs Haar-cascade subset or trained 7// classifier; blocks per-face landmark axes 8// - hand keypoint / finger skeleton: queued; needs hand- 9// specific pose model 10// - pose estimation (joint angles): queued; needs skeleton 11// fitter 12// - non-frontal pose support: v1 assumes roughly-upright 13// frontal body; lying poses + side-views need v2 14// 15// COVERED: 16// - Position-based blob labeling: largest blob -> torso; 17// highest blob (smallest y) above torso top -> head; 18// lateral blobs overlapping torso y-range -> arms (L/R by 19// x position); blobs below torso bottom -> legs (L/R by x) 20// - Single-person image assumption (caller filters to one 21// person upstream via downstream knn / face detection) 22// - Generic NxBBox output that downstream tier-axes consume 23// (nx_vein_signal / nx_subsurface_signal / etc.) 24// 25// Composes: 26// nx_segmenter_classical (produces blob list) 27// downstream tier-axis bricks (consumes the labeled regions) 28// 29// genealogy_id: substrate_anatomy_labeler_v1_2026_05_16 30// lineage_id: nx_landmark_anatomy_v1_heuristic 31 32import "nx_syscalls.nx" 33import "nx_runtime.nx" 34import "nx_tier.nx" 35import "nx_segmenter_classical.nx" 36const NX_MAGIC_1024: i64 = 1024 37const NX_MAGIC_3200: i64 = 3200 38const NX_MAGIC_64000: i64 = 64000 39const NX_MAGIC_30600: i64 = 30600 40const NX_MAGIC_19500: i64 = 19500 41const NX_MAGIC_160000: i64 = 160000 42 43// ===== bbox container ============================================ 44 45struct NxBBox { 46 valid: nx_int, 47 x0: nx_int, 48 y0: nx_int, 49 x1: nx_int, 50 y1: nx_int, 51} 52 53const NX_BBOX_BYTES: nx_size = 40 54 55// ===== landmarks result struct =================================== 56 57struct NxAnatomyLandmarks { 58 image_w: nx_int, 59 image_h: nx_int, 60 head: *NxBBox, 61 torso: *NxBBox, 62 arm_l: *NxBBox, 63 arm_r: *NxBBox, 64 leg_l: *NxBBox, 65 leg_r: *NxBBox, 66 n_arms_found: nx_int, 67 n_legs_found: nx_int, 68 has_head: nx_int, 69 has_torso: nx_int, 70 confidence_q10: nx_int, 71} 72 73const NX_ANATOMY_LANDMARKS_BYTES: nx_size = 96 74 75func _lm_alloc_bbox() -> *NxBBox { 76 let p: *u8 = sys_mmap(NX_BBOX_BYTES) 77 let b: *NxBBox = p as *NxBBox 78 b.valid = 0 79 b.x0 = 0 80 b.y0 = 0 81 b.x1 = 0 82 b.y1 = 0 83 return b 84} 85 86// ===== sealed-enum: detection mode =============================== 87 88const NX_LM_MODE_UPRIGHT_FRONTAL: nx_int = 0 89const NX_LM_MODE_RECLINING: nx_int = 1 // queued 90const NX_LM_MODE_PROFILE_SIDE: nx_int = 2 // queued 91const NX_LM_MODE_AUTO: nx_int = 3 // queued -- substrate picks 92 93// ===== helpers =================================================== 94 95func _lm_bbox_area(b: *NxBBox) -> nx_int { 96 if b.valid == 0 { return 0 } 97 let w: nx_int = b.x1 - b.x0 98 let h: nx_int = b.y1 - b.y0 99 if w <= 0 { return 0 } 100 if h <= 0 { return 0 } 101 return w * h 102} 103 104func _lm_bbox_cx(b: *NxBBox) -> nx_int { 105 return (b.x0 + b.x1) / 2 106} 107 108func _lm_bbox_cy(b: *NxBBox) -> nx_int { 109 return (b.y0 + b.y1) / 2 110} 111 112func _lm_blob_to_bbox(blob: *NxSegBlob, out: *NxBBox) -> nx_int { 113 out.valid = 1 114 out.x0 = blob.x0 115 out.y0 = blob.y0 116 out.x1 = blob.x1 117 out.y1 = blob.y1 118 return 0 119} 120 121func _lm_zero_bbox(b: *NxBBox) -> nx_int { 122 b.valid = 0 123 b.x0 = 0 124 b.y0 = 0 125 b.x1 = 0 126 b.y1 = 0 127 return 0 128} 129 130// ===== find largest blob ========================================= 131 132func _lm_largest_blob_idx(seg: *NxSegResult) -> nx_int { 133 if seg.n_blobs <= 0 { return -1 } 134 var best_idx: nx_int = 1 135 var best_size: nx_int = 0 136 var i: nx_int = 1 137 while i <= seg.n_blobs { 138 let b: *NxSegBlob = 139 (seg.blobs as *u8 + (i as nx_size) * NX_SEG_BLOB_BYTES) as *NxSegBlob 140 if b.pixel_count > best_size { 141 best_size = b.pixel_count 142 best_idx = i 143 } 144 i = i + 1 145 } 146 return best_idx 147} 148 149// ===== main label ================================================ 150// 151// Takes a segmenter result + image dimensions. Identifies the 152// largest blob as torso, then assigns other blobs to head / arms / 153// legs by position relative to torso. 154 155func nx_landmark_anatomy_label( 156 seg: *NxSegResult, image_w: nx_int, image_h: nx_int) -> *NxAnatomyLandmarks { 157 158 let r_ptr: *u8 = sys_mmap(NX_ANATOMY_LANDMARKS_BYTES) 159 let r: *NxAnatomyLandmarks = r_ptr as *NxAnatomyLandmarks 160 r.image_w = image_w 161 r.image_h = image_h 162 r.has_head = 0 163 r.has_torso = 0 164 r.n_arms_found = 0 165 r.n_legs_found = 0 166 r.confidence_q10 = 0 167 r.head = _lm_alloc_bbox() 168 r.torso = _lm_alloc_bbox() 169 r.arm_l = _lm_alloc_bbox() 170 r.arm_r = _lm_alloc_bbox() 171 r.leg_l = _lm_alloc_bbox() 172 r.leg_r = _lm_alloc_bbox() 173 174 if seg == (0 as *NxSegResult) { return r } 175 if seg.n_blobs < 1 { return r } 176 177 // ---- identify torso = largest blob ---- 178 let torso_idx: nx_int = _lm_largest_blob_idx(seg) 179 if torso_idx < 0 { return r } 180 let torso_blob: *NxSegBlob = 181 (seg.blobs as *u8 + (torso_idx as nx_size) * NX_SEG_BLOB_BYTES) as *NxSegBlob 182 _lm_blob_to_bbox(torso_blob, r.torso) 183 r.has_torso = 1 184 let torso_cx: nx_int = _lm_bbox_cx(r.torso) 185 let torso_top: nx_int = r.torso.y0 186 let torso_bot: nx_int = r.torso.y1 187 let torso_w: nx_int = r.torso.x1 - r.torso.x0 188 189 // ---- assign other blobs ---- 190 // 191 // For each non-torso blob, classify by position relative to torso: 192 // - centroid above torso top + cx within torso x-extent +-25%: 193 // HEAD candidate (pick highest) 194 // - bbox overlaps torso y-range but extends beyond torso x-extent: 195 // ARM candidate (L/R by cx vs torso_cx) 196 // - centroid below torso bottom: 197 // LEG candidate (L/R by cx) 198 // 199 // For each role we keep the BEST candidate (or 2 best for arms/legs). 200 201 let torso_x_tolerance: nx_int = (torso_w * 25) / 100 // 25% margin 202 let head_x_min: nx_int = r.torso.x0 - torso_x_tolerance 203 let head_x_max: nx_int = r.torso.x1 + torso_x_tolerance 204 205 // First pass: find best head (highest blob with centroid in 206 // torso x-extent). 207 var best_head_idx: nx_int = -1 208 var best_head_y: nx_int = image_h // smaller = higher = better 209 var i: nx_int = 1 210 while i <= seg.n_blobs { 211 if i != torso_idx { 212 let b: *NxSegBlob = 213 (seg.blobs as *u8 + (i as nx_size) * NX_SEG_BLOB_BYTES) as *NxSegBlob 214 let b_cx: nx_int = (b.x0 + b.x1) / 2 215 let b_cy: nx_int = (b.y0 + b.y1) / 2 216 if b_cy < torso_top { 217 if b_cx >= head_x_min { 218 if b_cx <= head_x_max { 219 if b_cy < best_head_y { 220 best_head_y = b_cy 221 best_head_idx = i 222 } 223 } 224 } 225 } 226 } 227 i = i + 1 228 } 229 if best_head_idx >= 0 { 230 let head_blob: *NxSegBlob = 231 (seg.blobs as *u8 + (best_head_idx as nx_size) * NX_SEG_BLOB_BYTES) as *NxSegBlob 232 _lm_blob_to_bbox(head_blob, r.head) 233 r.has_head = 1 234 } 235 236 // Second pass: find arms (best of left + best of right). 237 var best_arm_l_idx: nx_int = -1 238 var best_arm_l_size: nx_int = 0 239 var best_arm_r_idx: nx_int = -1 240 var best_arm_r_size: nx_int = 0 241 var best_leg_l_idx: nx_int = -1 242 var best_leg_l_size: nx_int = 0 243 var best_leg_r_idx: nx_int = -1 244 var best_leg_r_size: nx_int = 0 245 var j: nx_int = 1 246 while j <= seg.n_blobs { 247 if j != torso_idx { 248 if j != best_head_idx { 249 let b2: *NxSegBlob = 250 (seg.blobs as *u8 + (j as nx_size) * NX_SEG_BLOB_BYTES) as *NxSegBlob 251 let b_cx2: nx_int = (b2.x0 + b2.x1) / 2 252 let b_cy2: nx_int = (b2.y0 + b2.y1) / 2 253 // Arm: overlaps torso y-range OR is at-torso-level 254 // (cy between torso_top and torso_bot) AND extends 255 // beyond torso x-extent. 256 var is_arm: nx_int = 0 257 if b_cy2 >= torso_top { 258 if b_cy2 <= torso_bot { 259 if b_cx2 < r.torso.x0 { 260 is_arm = 1 261 } 262 if b_cx2 > r.torso.x1 { 263 is_arm = 1 264 } 265 } 266 } 267 if is_arm == 1 { 268 if b_cx2 < torso_cx { 269 if b2.pixel_count > best_arm_l_size { 270 best_arm_l_size = b2.pixel_count 271 best_arm_l_idx = j 272 } 273 } else { 274 if b2.pixel_count > best_arm_r_size { 275 best_arm_r_size = b2.pixel_count 276 best_arm_r_idx = j 277 } 278 } 279 } else { 280 // Leg: centroid below torso bottom. 281 if b_cy2 > torso_bot { 282 if b_cx2 < torso_cx { 283 if b2.pixel_count > best_leg_l_size { 284 best_leg_l_size = b2.pixel_count 285 best_leg_l_idx = j 286 } 287 } else { 288 if b2.pixel_count > best_leg_r_size { 289 best_leg_r_size = b2.pixel_count 290 best_leg_r_idx = j 291 } 292 } 293 } 294 } 295 } 296 } 297 j = j + 1 298 } 299 if best_arm_l_idx >= 0 { 300 let bb: *NxSegBlob = 301 (seg.blobs as *u8 + (best_arm_l_idx as nx_size) * NX_SEG_BLOB_BYTES) as *NxSegBlob 302 _lm_blob_to_bbox(bb, r.arm_l) 303 r.n_arms_found = r.n_arms_found + 1 304 } 305 if best_arm_r_idx >= 0 { 306 let bb2: *NxSegBlob = 307 (seg.blobs as *u8 + (best_arm_r_idx as nx_size) * NX_SEG_BLOB_BYTES) as *NxSegBlob 308 _lm_blob_to_bbox(bb2, r.arm_r) 309 r.n_arms_found = r.n_arms_found + 1 310 } 311 if best_leg_l_idx >= 0 { 312 let bb3: *NxSegBlob = 313 (seg.blobs as *u8 + (best_leg_l_idx as nx_size) * NX_SEG_BLOB_BYTES) as *NxSegBlob 314 _lm_blob_to_bbox(bb3, r.leg_l) 315 r.n_legs_found = r.n_legs_found + 1 316 } 317 if best_leg_r_idx >= 0 { 318 let bb4: *NxSegBlob = 319 (seg.blobs as *u8 + (best_leg_r_idx as nx_size) * NX_SEG_BLOB_BYTES) as *NxSegBlob 320 _lm_blob_to_bbox(bb4, r.leg_r) 321 r.n_legs_found = r.n_legs_found + 1 322 } 323 324 // Confidence proxy: 1024 if all parts found (head + torso + 325 // 2 arms + 2 legs); reduce for missing parts. 326 var confidence: nx_int = 0 327 if r.has_torso == 1 { confidence = confidence + 200 } 328 if r.has_head == 1 { confidence = confidence + 200 } 329 confidence = confidence + r.n_arms_found * 156 330 confidence = confidence + r.n_legs_found * 156 331 if confidence > NX_MAGIC_1024 { confidence = NX_MAGIC_1024 } 332 r.confidence_q10 = confidence 333 return r 334} 335 336// ===== self-test ================================================= 337 338func _lm_set_blob(arr: *NxSegBlob, idx: nx_int, 339 label: nx_int, x0: nx_int, y0: nx_int, 340 x1: nx_int, y1: nx_int, pix: nx_int) -> nx_int { 341 let b: *NxSegBlob = 342 (arr as *u8 + (idx as nx_size) * NX_SEG_BLOB_BYTES) as *NxSegBlob 343 b.label = label 344 b.x0 = x0 345 b.y0 = y0 346 b.x1 = x1 347 b.y1 = y1 348 b.pixel_count = pix 349 return 0 350} 351 352func main() -> nx_int { 353 // Build a synthetic segmenter result with 5 skin blobs forming 354 // a standing person on a 1024x1024 image: 355 // head: (490, 100) to (530, 180) -- small 356 // torso: (430, 200) to (590, 600) -- LARGE (this is largest) 357 // arm_l: (340, 220) to (430, 560) -- left of torso 358 // arm_r: (590, 220) to (680, 560) -- right of torso 359 // leg_l: (440, 600) to (505, 900) -- below torso 360 // leg_r: (515, 600) to (580, 900) -- below torso 361 // 362 // Slots 0 unused (label 0 = background); 1..6 are blobs. 363 364 let seg_ptr: *u8 = sys_mmap(NX_SEG_RESULT_BYTES) 365 let seg: *NxSegResult = seg_ptr as *NxSegResult 366 seg.n_blobs = 6 367 seg.width = NX_MAGIC_1024 368 seg.height = NX_MAGIC_1024 369 370 let blobs: *NxSegBlob = (sys_mmap(NX_SEG_BLOB_BYTES * 7)) as *NxSegBlob 371 _lm_set_blob(blobs, 0, 0, 0, 0, 0, 0, 0) // unused 372 _lm_set_blob(blobs, 1, 1, 490, 100, 530, 180, NX_MAGIC_3200) // head (small) 373 _lm_set_blob(blobs, 2, 2, 430, 200, 590, 600, NX_MAGIC_64000) // torso (largest) 374 _lm_set_blob(blobs, 3, 3, 340, 220, 430, 560, NX_MAGIC_30600) // arm_l 375 _lm_set_blob(blobs, 4, 4, 590, 220, 680, 560, NX_MAGIC_30600) // arm_r 376 _lm_set_blob(blobs, 5, 5, 440, 600, 505, 900, NX_MAGIC_19500) // leg_l 377 _lm_set_blob(blobs, 6, 6, 515, 600, 580, 900, NX_MAGIC_19500) // leg_r 378 seg.blobs = blobs 379 380 let r: *NxAnatomyLandmarks = nx_landmark_anatomy_label(seg, NX_MAGIC_1024, NX_MAGIC_1024) 381 if r == (0 as *NxAnatomyLandmarks) { return 1 } 382 if r.has_torso != 1 { return 2 } 383 if r.torso.x0 != 430 { return 3 } 384 if r.torso.x1 != 590 { return 4 } 385 if r.has_head != 1 { return 5 } 386 if r.head.y0 != 100 { return 6 } 387 if r.n_arms_found != 2 { return 7 } 388 if r.arm_l.x0 != 340 { return 8 } // left arm 389 if r.arm_r.x0 != 590 { return 9 } // right arm 390 if r.n_legs_found != 2 { return 10 } 391 if r.leg_l.x0 != 440 { return 11 } 392 if r.leg_r.x0 != 515 { return 12 } 393 if r.confidence_q10 < 1000 { return 13 } // should be full 394 395 // ---- Single blob: torso only, no head/arms/legs ---- 396 let seg2_ptr: *u8 = sys_mmap(NX_SEG_RESULT_BYTES) 397 let seg2: *NxSegResult = seg2_ptr as *NxSegResult 398 seg2.n_blobs = 1 399 let blobs2: *NxSegBlob = (sys_mmap(NX_SEG_BLOB_BYTES * 2)) as *NxSegBlob 400 _lm_set_blob(blobs2, 0, 0, 0, 0, 0, 0, 0) 401 _lm_set_blob(blobs2, 1, 1, 100, 100, 500, 500, NX_MAGIC_160000) 402 seg2.blobs = blobs2 403 let r2: *NxAnatomyLandmarks = nx_landmark_anatomy_label(seg2, NX_MAGIC_1024, NX_MAGIC_1024) 404 if r2.has_torso != 1 { return 20 } 405 if r2.has_head != 0 { return 21 } 406 if r2.n_arms_found != 0 { return 22 } 407 if r2.n_legs_found != 0 { return 23 } 408 if r2.confidence_q10 > 600 { return 24 } // only torso -> reduced 409 410 // ---- Zero blobs ---- 411 let seg3_ptr: *u8 = sys_mmap(NX_SEG_RESULT_BYTES) 412 let seg3: *NxSegResult = seg3_ptr as *NxSegResult 413 seg3.n_blobs = 0 414 let r3: *NxAnatomyLandmarks = nx_landmark_anatomy_label(seg3, NX_MAGIC_1024, NX_MAGIC_1024) 415 if r3.has_torso != 0 { return 30 } 416 if r3.confidence_q10 != 0 { return 31 } 417 418 return 0 419}