code wiki / (root) / nx_anatomy_measure.nx

nx_anatomy_measure.nx source

↩ module page · 340 lines · 14987 B

1// nx_anatomy_measure.nx -- measure named anatomical axes from binary mask. 2// 3// Composes existing primitives (no duplication): 4// - nx_color_skin_mask (already in nx_color_v2) 5// - nx_region_segment (already in nx_region) 6// - nx_eye_detect (Layer 7, KEYSTONE) 7// 8// Cross-section measurement is the central technique here: 9// - bust = widest mask-width across upper torso Y-band 10// - waist = narrowest mask-width across mid torso Y-band 11// - hip = widest mask-width across lower torso Y-band 12// - navel = darkest pixel along torso vertical midline 13// 14// Output: flat-array (axis_id, value_px) bundle ready for 15// nx_beauty_score(). All primitives are flat-array-API to avoid the 16// nxc2 codegen bug with struct-pointer returns. 17 18// nx_safety_envelope: 19// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 20// sil_target: SIL1 21// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 22// verdict: NOT_YET_EVALUATED 23 24import "nx_syscalls.nx" 25import "nx_image.nx" 26import "nx_canon_proportions.nx" 27import "nx_beauty_score.nx" 28const K_MAGIC_9999: i64 = 9999 29 30// Count consecutive skin pixels horizontally at row y within [x0, x1). 31// Returns the LONGEST run found. 32func nx_mask_longest_run_at_y(mask: *Image, y: i64, x0: i64, x1: i64) -> i64 { 33 if y < 0 { return 0 } 34 if y >= mask.height { return 0 } 35 var best: i64 = 0 36 var cur: i64 = 0 37 var x: i64 = x0 38 while x < x1 { 39 let v: i64 = nx_image_get(mask, x, y, 0) 40 if v > 0 { 41 cur = cur + 1 42 if cur > best { best = cur } 43 } else { 44 cur = 0 45 } 46 x = x + 1 47 } 48 return best 49} 50 51// Find Y in [y0, y1) where mask is WIDEST. Returns (y, width) packed. 52// Out: out_y receives the Y, returns the width. 53func nx_mask_widest_y(mask: *Image, y0: i64, y1: i64, 54 x0: i64, x1: i64, out_y: *i64) -> i64 { 55 var best_w: i64 = 0 56 var best_y: i64 = y0 57 var y: i64 = y0 58 while y < y1 { 59 let w: i64 = nx_mask_longest_run_at_y(mask, y, x0, x1) 60 if w > best_w { best_w = w; best_y = y } 61 y = y + 1 62 } 63 out_y[0] = best_y 64 return best_w 65} 66 67// Find Y in [y0, y1) where mask is NARROWEST (but nonzero). Returns 68// (out_y, width). 69func nx_mask_narrowest_y(mask: *Image, y0: i64, y1: i64, 70 x0: i64, x1: i64, out_y: *i64) -> i64 { 71 var best_w: i64 = K_MAGIC_9999 72 var best_y: i64 = y0 73 var found: i64 = 0 74 var y: i64 = y0 75 while y < y1 { 76 let w: i64 = nx_mask_longest_run_at_y(mask, y, x0, x1) 77 if w > 0 { 78 if w < best_w { best_w = w; best_y = y; found = 1 } 79 } 80 y = y + 1 81 } 82 out_y[0] = best_y 83 if found == 0 { return 0 } 84 return best_w 85} 86 87// Measure standard body axes from a skin mask + torso bbox + eye width. 88// Caller pre-allocates measurement bundle (NX_MEAS_FIELDS * count i64). 89// Returns number of (axis, value) pairs written. 90// 91// Inputs: 92// mask: full-frame skin mask 93// eye_width_px: from nx_eye_detect 94// face_min_x..face_max_y: face bbox from nx_region 95// torso_min_x..torso_max_y: torso bbox from nx_region 96// meas: output flat-array, layout per nx_beauty_score's NX_MEAS_FIELDS 97func nx_anatomy_measure_body(mask: *Image, eye_width_px: i64, 98 face_min_x: i64, face_min_y: i64, 99 face_max_x: i64, face_max_y: i64, 100 torso_min_x: i64, torso_min_y: i64, 101 torso_max_x: i64, torso_max_y: i64, 102 meas: *i64) -> i64 { 103 var n: i64 = 0 104 let face_w: i64 = face_max_x - face_min_x + 1 105 let face_h: i64 = face_max_y - face_min_y + 1 106 let torso_h: i64 = torso_max_y - torso_min_y + 1 107 108 // Keystone. 109 meas[n * NX_MEAS_FIELDS] = NX_AXIS_EYE_WIDTH 110 meas[n * NX_MEAS_FIELDS + 1] = eye_width_px 111 n = n + 1 112 113 // Face dimensions (bbox-derived). 114 meas[n * NX_MEAS_FIELDS] = NX_AXIS_FACE_WIDTH 115 meas[n * NX_MEAS_FIELDS + 1] = face_w 116 n = n + 1 117 meas[n * NX_MEAS_FIELDS] = NX_AXIS_FACE_HEIGHT 118 meas[n * NX_MEAS_FIELDS + 1] = face_h 119 n = n + 1 120 121 // Bust = widest mask row in UPPER third of torso. 122 let bust_y0: i64 = torso_min_y 123 let bust_y1: i64 = torso_min_y + torso_h / 3 124 let bust_y_out: *i64 = (sys_mmap(8 + 8)) as *i64 125 let bust_w: i64 = nx_mask_widest_y(mask, bust_y0, bust_y1, 126 torso_min_x, torso_max_x + 1, bust_y_out) 127 if bust_w > 0 { 128 meas[n * NX_MEAS_FIELDS] = NX_AXIS_BUST_WIDTH 129 meas[n * NX_MEAS_FIELDS + 1] = bust_w 130 n = n + 1 131 } 132 133 // Waist = narrowest mask row in MIDDLE third of torso. 134 let waist_y0: i64 = torso_min_y + torso_h / 3 135 let waist_y1: i64 = torso_min_y + (torso_h * 2) / 3 136 let waist_y_out: *i64 = (sys_mmap(8 + 8)) as *i64 137 let waist_w: i64 = nx_mask_narrowest_y(mask, waist_y0, waist_y1, 138 torso_min_x, torso_max_x + 1, waist_y_out) 139 if waist_w > 0 { 140 meas[n * NX_MEAS_FIELDS] = NX_AXIS_WAIST_WIDTH 141 meas[n * NX_MEAS_FIELDS + 1] = waist_w 142 n = n + 1 143 } 144 145 // Hip = widest mask row in LOWER third of torso. 146 let hip_y0: i64 = torso_min_y + (torso_h * 2) / 3 147 let hip_y1: i64 = torso_max_y + 1 148 let hip_y_out: *i64 = (sys_mmap(8 + 8)) as *i64 149 let hip_w: i64 = nx_mask_widest_y(mask, hip_y0, hip_y1, 150 torso_min_x, torso_max_x + 1, hip_y_out) 151 if hip_w > 0 { 152 meas[n * NX_MEAS_FIELDS] = NX_AXIS_HIP_WIDTH 153 meas[n * NX_MEAS_FIELDS + 1] = hip_w 154 n = n + 1 155 } 156 157 // Shoulder width: widest row in TOP 15% of torso, where pectoral fold 158 // is largest. For a face-on subject this approximates shoulder span. 159 let shoulder_y0: i64 = torso_min_y 160 let shoulder_y1: i64 = torso_min_y + torso_h / 6 161 let shoulder_y_out: *i64 = (sys_mmap(8 + 8)) as *i64 162 let shoulder_w: i64 = nx_mask_widest_y(mask, shoulder_y0, shoulder_y1, 163 torso_min_x, torso_max_x + 1, shoulder_y_out) 164 if shoulder_w > 0 { 165 meas[n * NX_MEAS_FIELDS] = NX_AXIS_SHOULDER_WIDTH 166 meas[n * NX_MEAS_FIELDS + 1] = shoulder_w 167 n = n + 1 168 } 169 170 return n 171} 172 173// ============================================================================ 174// AT2 (aesthetictwin rung): OUTLINE CURVINESS -- the measure the published 175// evidence prefers to the waist-to-hip ratio. 176// 177// WHY THIS IS NOT A SECOND WHR, AND THE REASON IS MECHANICAL. On line drawings 178// that varied curviness and width independently, curviness predicted the rated 179// attractiveness of a woman's body BETTER than the waist-to-hip ratio, and the 180// authors state that the two are not uniquely related (Hubner and Ufken 2024, 181// pinned as aesthetictwin.refs key hubner2024, whose grounds field already 182// names this function as the contract). The reason is the whole point here: 183// A RATIO IS SCALE-INVARIANT AND A CURVATURE IS NOT. Two silhouettes can share 184// a waist-to-hip ratio EXACTLY while one turns twice as sharply, because 185// curvature carries a 1/length -- double every dimension and the curvature 186// halves. So this separates bodies the WHR reports as identical, which is the 187// discrimination the raters were making and the one this board could not 188// previously express at all. 189// 190// METHOD. Over the row band take the mask run width w(y) -- the SAME primitive 191// nx_mask_widest_y and nx_mask_narrowest_y already use, so this organ keeps one 192// width measure and does not grow a second ruler beside it. Smooth with a 193// 3-row box, because a mask edge jitters by a pixel and an unsmoothed second 194// difference would measure that jitter rather than the body. Then integrate the 195// absolute discrete second difference of the smoothed profile. 196// 197// WHY IT IS NOT A SECOND WHR, THE OTHER HALF: A RATIO IS BLIND TO WHERE THE 198// TAPER HAPPENS. Two bodies can carry the SAME waist and the SAME hip -- so the 199// same ratio to the permil -- while one reaches its waist through a short sharp 200// pinch and the other through a long gentle sweep. Those are different amounts 201// of curviness and the ratio cannot see the difference at all. That is the 202// discrimination this function exists to supply. 203// 204// REPORTED THREE WAYS, BECAUSE AN AGGREGATE WITHOUT ITS DENOMINATOR IS NOT 205// PUBLISHABLE AND BECAUSE THE FIGURES ANSWER DIFFERENT QUESTIONS: 206// O_TOTAL -- TOTAL TURNING of the outline, in milli-slope units. This is the 207// curviness and it is what the function RETURNS. It is the total 208// change in the outline's SLOPE, and a slope is dimensionless, so 209// it is INVARIANT UNDER UNIFORM SCALE: photograph the same body 210// twice as large and this number does not move. That invariance is 211// a REQUIREMENT, not a side effect -- curviness is a property of 212// SHAPE, and a measure that rose merely because a body was smaller 213// would be reporting size wearing the name of shape. 214// O_ROWS -- how many BODY ROWS fed the measurement. Never read a total 215// without its denominator. 216// O_SEGS -- how many interior segments contributed a second difference. 217// Fixed by construction, and reported precisely so a reader can 218// SEE that the term count does not vary with the body's size. 219// 220// HOW THE INVARIANCE IS OBTAINED, BECAUSE IT DOES NOT COME FOR FREE. The width 221// profile is RESAMPLED onto a FIXED number of segments spanning the body, and 222// each segment's mean width is then divided by the body's OWN height. Both 223// steps are load-bearing: the fixed segment count makes the NUMBER OF TERMS 224// independent of how many pixels tall the body happens to be, and dividing 225// width by height makes each term DIMENSIONLESS. Together they mean the same 226// shape photographed at any size yields the same number. 227// 228// WARNING -- TWO EARLIER DRAFTS GOT THIS WRONG AND THE GATE CAUGHT BOTH, WHICH 229// IS THE ONLY REASON THIS COMMENT IS TRUSTWORTHY. The first returned a per-row 230// density and argued that scale-SENSITIVITY was the point; the rung's done-rule, 231// written into aesthetictwin.plan before any code, refuted it. The second 232// integrated the second difference ROW BY ROW -- which is invariant in exact 233// arithmetic and is NOT invariant in integers, because every rounded row adds a 234// little quantisation noise and a body twice as tall accumulates twice as much 235// of it. MEASURED: the same shape at twice the scale read 13333 against 18666, 236// a 40 percent error no fixture could have removed, because the defect was in 237// the measure. Resampling to a fixed segment count removes it at the root: each 238// segment averages many rows so the rounding averages out, and the count of 239// terms stops depending on size. 240// 241// DECLARED IMPRECISION, so the next reader does not trust this as exact: this is 242// a discrete second difference over a COARSE resampling -- the paper's own 243// relatively simple curvature-based measure -- and NOT the exact 244// second-derivative form normalised by the slope. Its absolute value therefore 245// depends on the chosen segment count, so this number is comparable ACROSS 246// BODIES and NOT against any externally published curvature figure. Ordering and 247// invariance, the two properties the done-rule asks for, are unaffected. 248// 249// UNMEASURED IS A REAL ANSWER AND IT IS NOT ZERO. A body too short to give two 250// rows per segment cannot be resampled at all and returns AM_CURV_UNMEASURED, 251// because no-evidence and measured-zero are different claims and publishing the 252// first as the second is a lie the caller cannot see. 253// ============================================================================ 254const AM_CURV_UNMEASURED: i64 = 0 - 1 255// The profile is resampled onto this many equal shares of the body. 16 is 256// chosen so the shortest body this organ will accept still gives 2 rows per 257// segment, while a full-height torso gives dozens -- enough averaging to bury 258// pixel rounding without smoothing away a waist. MINROWS follows FROM it. 259const AM_CURV_SEGS: i64 = 16 260const AM_CURV_MINROWS: i64 = 32 261const AM_CURV_UNIT: i64 = 1000000 262const AM_CURV_PERMIL: i64 = 1000 263const AM_CURV_O_TOTAL: i64 = 0 264const AM_CURV_O_ROWS: i64 = 1 265const AM_CURV_O_SEGS: i64 = 2 266const AM_CURV_O_FIELDS: i64 = 3 267const AM_CURV_SLACK: i64 = 16 268 269func am_outline_curvature(mask: *Image, y0: i64, y1: i64, 270 x0: i64, x1: i64, out: *i64) -> i64 { 271 out[AM_CURV_O_TOTAL] = 0 272 out[AM_CURV_O_ROWS] = 0 273 out[AM_CURV_O_SEGS] = AM_CURV_UNMEASURED 274 let band: i64 = y1 - y0 275 if band < AM_CURV_MINROWS { return AM_CURV_UNMEASURED } 276 let w: *i64 = (sys_mmap(band * 8 + AM_CURV_SLACK)) as *i64 277 var i: i64 = 0 278 while i < band { 279 w[i] = nx_mask_longest_run_at_y(mask, y0 + i, x0, x1) 280 i = i + 1 281 } 282 // The body is the FIRST CONTIGUOUS RUN of non-empty rows. A zero row inside 283 // the band is background, and a second difference taken across that edge 284 // would measure where the band was placed rather than how the body turns. 285 var f: i64 = 0 - 1 286 i = 0 287 while i < band { 288 if w[i] > 0 { if f < 0 { f = i } } 289 i = i + 1 290 } 291 if f < 0 { return AM_CURV_UNMEASURED } 292 var l: i64 = f 293 var scanning: i64 = 1 294 while scanning == 1 { 295 scanning = 0 296 if l < band { 297 if w[l] > 0 { l = l + 1; scanning = 1 } 298 } 299 } 300 let n: i64 = l - f 301 if n < AM_CURV_MINROWS { return AM_CURV_UNMEASURED } 302 // RESAMPLE onto a FIXED number of segments spanning the body, and carry each 303 // segment as its MEAN WIDTH DIVIDED BY THE BODY'S OWN HEIGHT in AM_CURV_UNIT 304 // fixed point. The division by n is what makes each term dimensionless, and 305 // the fixed segment count is what makes the number of terms independent of 306 // the body's pixel size. Neither is optional: drop either one and the same 307 // shape at a different scale reads a different number. 308 let seg: *i64 = (sys_mmap(AM_CURV_SEGS * 8 + AM_CURV_SLACK)) as *i64 309 var j: i64 = 0 310 while j < AM_CURV_SEGS { 311 let a: i64 = f + (n * j) / AM_CURV_SEGS 312 var b: i64 = f + (n * (j + 1)) / AM_CURV_SEGS 313 if b <= a { b = a + 1 } 314 var s: i64 = 0 315 var c: i64 = 0 316 var y: i64 = a 317 while y < b { 318 s = s + w[y] 319 c = c + 1 320 y = y + 1 321 } 322 seg[j] = (s * AM_CURV_UNIT) / (c * n) 323 j = j + 1 324 } 325 var total: i64 = 0 326 var used: i64 = 0 327 j = 1 328 while j < AM_CURV_SEGS - 1 { 329 var d: i64 = seg[j - 1] + seg[j + 1] - seg[j] - seg[j] 330 if d < 0 { d = 0 - d } 331 total = total + d 332 used = used + 1 333 j = j + 1 334 } 335 if used < 1 { return AM_CURV_UNMEASURED } 336 out[AM_CURV_O_TOTAL] = total 337 out[AM_CURV_O_ROWS] = n 338 out[AM_CURV_O_SEGS] = used 339 return total 340}