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}