code wiki / (root) / nx_image_grade_v1.nx

nx_image_grade_v1.nx source

↩ module page · 488 lines · 17233 B

1// nx_image_grade_v1.nx -- skin-localized image grading composer. 2// 3// CAPABILITY_COMPLETENESS: PARTIAL 4// MISSING_CAPABILITIES: 5// - face landmark detection (eye / mouth / nose / wrist / 6// inside-elbow keypoints): queued; needed for the still- 7// PENDING per-landmark axes (vein-signal at wrist, 8// subsurface at fingertips, micro-expression at face, etc.) 9// - VLM grader (the second triangulation source) 10// - hand-specific segmentation: skin blob detection is generic; 11// hand vs face vs torso disambiguation queued 12// 13// COVERED (PROMOTED FROM v0): 14// - Skin-localized mean R/G/B via inline Kovac skin mask 15// - Lab + ITA + undertone computed on SKIN PIXELS ONLY 16// (no longer biased by clothing / background) 17// - Skin coverage fraction (skin_pixels / total_pixels Q10) 18// - Connected-component segmentation of skin region 19// - Largest skin blob bbox -> caller can invoke per-region 20// graders against it 21// 22// Composes: 23// - nx_png_decoder + nx_jpeg_decoder (auto-detect by magic) 24// - inline Kovac skin-pixel test (per Kovac 2003) 25// - nx_segmenter_classical (connected-component labeling) 26// - nx_lab_from_rgb (CIELab Q10) 27// - nx_skin_tone_ita + nx_undertone_classify 28// - nx_verdict_phrase 29// 30// genealogy_id: substrate_image_grade_v1_skin_localized_2026_05_16 31// lineage_id: nx_image_grade_v1 32 33// nx_safety_envelope: 34// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 35// sil_target: SIL1 36// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 37// verdict: NOT_YET_EVALUATED 38 39import "nx_syscalls.nx" 40import "nx_runtime.nx" 41import "nx_tier.nx" 42import "nx_png_decoder.nx" 43import "nx_jpeg_decoder.nx" 44import "nx_verdict_phrase.nx" 45import "nx_lab_from_rgb.nx" 46import "nx_skin_tone_ita.nx" 47import "nx_undertone_class.nx" 48import "nx_segmenter_classical.nx" 49const NX_MAGIC_1024: i64 = 1024 50 51// ===== sealed: format + error codes (same as v0) ================= 52 53const NX_IGV1_FORMAT_UNKNOWN: nx_int = 0 54const NX_IGV1_FORMAT_PNG: nx_int = 1 55const NX_IGV1_FORMAT_JPEG: nx_int = 2 56 57const NX_IGV1_OK: nx_int = 0 58const NX_IGV1_ERR_TOO_SHORT: nx_int = 1 59const NX_IGV1_ERR_UNKNOWN_FORMAT: nx_int = 2 60const NX_IGV1_ERR_DECODER: nx_int = 3 61const NX_IGV1_ERR_NULL_PIXELS: nx_int = 4 62 63// ===== per-axis status (same as v0) ============================== 64 65const NX_IGV1_AXIS_OK: nx_int = 0 66const NX_IGV1_AXIS_PENDING_SEGMENTATION: nx_int = 1 67const NX_IGV1_AXIS_PENDING_LANDMARK: nx_int = 2 68const NX_IGV1_AXIS_PENDING_VLM: nx_int = 3 69const NX_IGV1_AXIS_NOT_APPLICABLE: nx_int = 4 70const NX_IGV1_AXIS_NO_SKIN_DETECTED: nx_int = 5 71 72// ===== per-axis verdict ========================================== 73 74struct NxIgv1AxisVerdict { 75 axis_id: nx_int, 76 status: nx_int, 77 verdict_value: nx_int, 78 score_q10: nx_int, 79 phrase_buf: *u8, 80 phrase_len: nx_int, 81} 82 83const NX_IGV1_AXIS_VERDICT_BYTES: nx_size = 48 84 85// ===== top-level report ========================================== 86 87struct NxIgv1Report { 88 source_format: nx_int, 89 width: nx_int, 90 height: nx_int, 91 error_code: nx_int, 92 n_axes: nx_int, 93 axes: *NxIgv1AxisVerdict, 94 // Whole-image stats 95 mean_r: nx_int, 96 mean_g: nx_int, 97 mean_b: nx_int, 98 // Skin-localized stats (the v1 promotion) 99 skin_pixel_count: nx_int, 100 skin_mean_r: nx_int, 101 skin_mean_g: nx_int, 102 skin_mean_b: nx_int, 103 skin_coverage_q10: nx_int, 104 // Largest skin blob 105 n_skin_blobs: nx_int, 106 largest_blob_x0: nx_int, 107 largest_blob_y0: nx_int, 108 largest_blob_x1: nx_int, 109 largest_blob_y1: nx_int, 110 largest_blob_pixels: nx_int, 111} 112 113const NX_IGV1_REPORT_BYTES: nx_size = 144 114 115// Axis IDs (same as v0). 116const NX_IGV1_AX_DIMENSIONS: nx_int = 0 117const NX_IGV1_AX_LUMINANCE: nx_int = 1 118const NX_IGV1_AX_SKIN_TONE_ITA: nx_int = 4 119const NX_IGV1_AX_GLCM_TEXTURE: nx_int = 5 120const NX_IGV1_AX_PORE_DETAIL: nx_int = 7 121const NX_IGV1_AX_UNDERTONE: nx_int = 12 122const NX_IGV1_AX_VEIN_SIGNAL: nx_int = 16 123const NX_IGV1_AX_SUBSURFACE_SIGNAL: nx_int = 17 124const NX_IGV1_AX_MICRO_EXPRESSION: nx_int = 22 125const NX_IGV1_AX_SHADOW_DIRECTION: nx_int = 23 126const NX_IGV1_AX_FINGER_COUNT: nx_int = 24 127const NX_IGV1_AX_SURFACE_PLANE: nx_int = 27 128const NX_IGV1_AX_SKIN_COVERAGE: nx_int = 100 // v1 addition 129 130const NX_IGV1_N_AXES: nx_int = 13 131 132// ===== format detect (same as v0) ================================ 133 134func _igv1_detect_format(buf: *u8, n: nx_int) -> nx_int { 135 if n < 4 { return NX_IGV1_FORMAT_UNKNOWN } 136 if buf[0] == (137 as u8) { 137 if buf[1] == (80 as u8) { 138 if buf[2] == (78 as u8) { 139 if buf[3] == (71 as u8) { 140 return NX_IGV1_FORMAT_PNG 141 } 142 } 143 } 144 } 145 if buf[0] == (255 as u8) { 146 if buf[1] == (216 as u8) { 147 return NX_IGV1_FORMAT_JPEG 148 } 149 } 150 return NX_IGV1_FORMAT_UNKNOWN 151} 152 153// ===== Kovac skin-pixel test ===================================== 154// 155// Per Kovac/Solina 2003 (Human Skin Colour Clustering for Face 156// Detection): RGB-space rules for typical-skin-tone pixels. 157// 158// R > 95 AND G > 40 AND B > 20 159// AND max(R, G, B) - min(R, G, B) > 15 160// AND |R - G| > 15 161// AND R > G AND R > B 162// 163// Substrate constraint: works on the bright-light rule set. A 164// secondary dim-light rule set is queued for v2. 165 166func _igv1_is_skin_pixel(r: nx_int, g: nx_int, b: nx_int) -> nx_int { 167 if r <= 95 { return 0 } 168 if g <= 40 { return 0 } 169 if b <= 20 { return 0 } 170 var max_v: nx_int = r 171 if g > max_v { max_v = g } 172 if b > max_v { max_v = b } 173 var min_v: nx_int = r 174 if g < min_v { min_v = g } 175 if b < min_v { min_v = b } 176 if (max_v - min_v) <= 15 { return 0 } 177 var rg_diff: nx_int = r - g 178 if rg_diff < 0 { rg_diff = 0 - rg_diff } 179 if rg_diff <= 15 { return 0 } 180 if r <= g { return 0 } 181 if r <= b { return 0 } 182 return 1 183} 184 185// ===== axis filler =============================================== 186 187func _igv1_set_axis(axes: *NxIgv1AxisVerdict, idx: nx_int, 188 axis_id: nx_int, status: nx_int, 189 verdict_value: nx_int, score_q10: nx_int) -> nx_int { 190 let slot: *NxIgv1AxisVerdict = 191 (axes as *u8 + (idx as nx_size) * NX_IGV1_AXIS_VERDICT_BYTES) as *NxIgv1AxisVerdict 192 slot.axis_id = axis_id 193 slot.status = status 194 slot.verdict_value = verdict_value 195 slot.score_q10 = score_q10 196 let phrase_buf: *u8 = (sys_mmap(128)) as *u8 197 if status == NX_IGV1_AXIS_OK { 198 let p: *u8 = nx_verdict_phrase_for(axis_id, verdict_value, NX_VP_MODE_TERSE) 199 var c: nx_int = 0 200 var safety: nx_int = 0 201 while safety < 127 { 202 let ch: nx_int = (p[c] as nx_int) & 255 203 if ch == 0 { break } 204 phrase_buf[c] = ch as u8 205 c = c + 1 206 safety = safety + 1 207 } 208 phrase_buf[c] = 0 as u8 209 slot.phrase_buf = phrase_buf 210 slot.phrase_len = c 211 } else { 212 var pmsg: *u8 = 0 as *u8 213 if status == NX_IGV1_AXIS_PENDING_SEGMENTATION { 214 pmsg = "pending segmenter" as *u8 215 } 216 if status == NX_IGV1_AXIS_PENDING_LANDMARK { 217 pmsg = "pending landmark detection" as *u8 218 } 219 if status == NX_IGV1_AXIS_PENDING_VLM { 220 pmsg = "pending vlm grader" as *u8 221 } 222 if status == NX_IGV1_AXIS_NOT_APPLICABLE { 223 pmsg = "not applicable" as *u8 224 } 225 if status == NX_IGV1_AXIS_NO_SKIN_DETECTED { 226 pmsg = "no skin pixels detected" as *u8 227 } 228 if pmsg == (0 as *u8) { pmsg = "unknown status" as *u8 } 229 var c2: nx_int = 0 230 var s2: nx_int = 0 231 while s2 < 127 { 232 let ch2: nx_int = (pmsg[c2] as nx_int) & 255 233 if ch2 == 0 { break } 234 phrase_buf[c2] = ch2 as u8 235 c2 = c2 + 1 236 s2 = s2 + 1 237 } 238 phrase_buf[c2] = 0 as u8 239 slot.phrase_buf = phrase_buf 240 slot.phrase_len = c2 241 } 242 return 0 243} 244 245// ===== top-level trigger ========================================= 246 247func nx_image_grade_v1(input: *u8, input_size: nx_int) -> *NxIgv1Report { 248 let r_ptr: *u8 = sys_mmap(NX_IGV1_REPORT_BYTES) 249 let r: *NxIgv1Report = r_ptr as *NxIgv1Report 250 r.error_code = NX_IGV1_OK 251 r.source_format = NX_IGV1_FORMAT_UNKNOWN 252 r.skin_pixel_count = 0 253 254 if input_size < 8 { 255 r.error_code = NX_IGV1_ERR_TOO_SHORT 256 return r 257 } 258 259 let fmt: nx_int = _igv1_detect_format(input, input_size) 260 r.source_format = fmt 261 if fmt == NX_IGV1_FORMAT_UNKNOWN { 262 r.error_code = NX_IGV1_ERR_UNKNOWN_FORMAT 263 return r 264 } 265 266 let axes_bytes: nx_size = (NX_IGV1_N_AXES as nx_size) * NX_IGV1_AXIS_VERDICT_BYTES 267 let axes: *NxIgv1AxisVerdict = (sys_mmap(axes_bytes)) as *NxIgv1AxisVerdict 268 r.axes = axes 269 r.n_axes = NX_IGV1_N_AXES 270 271 var pixels: *u8 = 0 as *u8 272 var n_ch: nx_int = 0 273 var bpp: nx_int = 0 274 var w: nx_int = 0 275 var h: nx_int = 0 276 277 if fmt == NX_IGV1_FORMAT_PNG { 278 let png_r: *NxPngResult = nx_png_decode(input, input_size) 279 if png_r == (0 as *NxPngResult) { 280 r.error_code = NX_IGV1_ERR_DECODER 281 return r 282 } 283 if png_r.error_code != NX_PNG_OK { 284 r.error_code = NX_IGV1_ERR_DECODER 285 return r 286 } 287 pixels = png_r.pixels 288 n_ch = png_r.n_channels 289 bpp = png_r.bytes_per_pix 290 if png_r.header != (0 as *NxPngHeader) { 291 w = png_r.header.width 292 h = png_r.header.height 293 } 294 } 295 if fmt == NX_IGV1_FORMAT_JPEG { 296 let jpg_r: *NxJpgResult = nx_jpeg_decode(input, input_size) 297 if jpg_r == (0 as *NxJpgResult) { 298 r.error_code = NX_IGV1_ERR_DECODER 299 return r 300 } 301 if jpg_r.error_code != NX_JPG_OK { 302 r.error_code = NX_IGV1_ERR_DECODER 303 return r 304 } 305 pixels = jpg_r.rgb 306 n_ch = 3 307 bpp = 3 308 w = jpg_r.width 309 h = jpg_r.height 310 } 311 if pixels == (0 as *u8) { 312 r.error_code = NX_IGV1_ERR_NULL_PIXELS 313 return r 314 } 315 r.width = w 316 r.height = h 317 318 // Single-pass: whole-image RGB accumulation + skin-mask 319 // construction + skin-pixel RGB accumulation. 320 let n_pixels: nx_int = w * h 321 let mask_bytes: nx_size = (n_pixels as nx_size) * 8 322 let skin_mask: *nx_int = (sys_mmap(mask_bytes)) as *nx_int 323 var sum_r: nx_int = 0 324 var sum_g: nx_int = 0 325 var sum_b: nx_int = 0 326 var sum_sr: nx_int = 0 327 var sum_sg: nx_int = 0 328 var sum_sb: nx_int = 0 329 var n_skin: nx_int = 0 330 var y: nx_int = 0 331 while y < h { 332 var x: nx_int = 0 333 while x < w { 334 let pix_off: nx_int = (y * w + x) * bpp 335 var r_val: nx_int = (pixels[pix_off] as nx_int) & 255 336 var g_val: nx_int = r_val 337 var b_val: nx_int = r_val 338 if n_ch >= 3 { 339 g_val = (pixels[pix_off + 1] as nx_int) & 255 340 b_val = (pixels[pix_off + 2] as nx_int) & 255 341 } 342 sum_r = sum_r + r_val 343 sum_g = sum_g + g_val 344 sum_b = sum_b + b_val 345 let pix_idx: nx_int = y * w + x 346 let is_skin: nx_int = _igv1_is_skin_pixel(r_val, g_val, b_val) 347 skin_mask[pix_idx] = is_skin 348 if is_skin == 1 { 349 sum_sr = sum_sr + r_val 350 sum_sg = sum_sg + g_val 351 sum_sb = sum_sb + b_val 352 n_skin = n_skin + 1 353 } 354 x = x + 1 355 } 356 y = y + 1 357 } 358 if n_pixels <= 0 { 359 r.error_code = NX_IGV1_ERR_NULL_PIXELS 360 return r 361 } 362 r.mean_r = sum_r / n_pixels 363 r.mean_g = sum_g / n_pixels 364 r.mean_b = sum_b / n_pixels 365 r.skin_pixel_count = n_skin 366 r.skin_coverage_q10 = (n_skin * NX_MAGIC_1024) / n_pixels 367 368 // Run segmenter on the skin mask. 369 let seg_r: *NxSegResult = nx_segmenter_connected(skin_mask, w, h) 370 if seg_r != (0 as *NxSegResult) { 371 r.n_skin_blobs = seg_r.n_blobs 372 if seg_r.n_blobs >= 1 { 373 let largest: *NxSegBlob = nx_segmenter_largest_blob(seg_r) 374 if largest != (0 as *NxSegBlob) { 375 r.largest_blob_x0 = largest.x0 376 r.largest_blob_y0 = largest.y0 377 r.largest_blob_x1 = largest.x1 378 r.largest_blob_y1 = largest.y1 379 r.largest_blob_pixels = largest.pixel_count 380 } 381 } 382 } 383 384 // Skin-localized chroma classification. 385 let no_skin: nx_int = 0 386 if n_skin <= 0 { 387 // No skin detected. Mark chroma axes appropriately. 388 r.skin_mean_r = 0 389 r.skin_mean_g = 0 390 r.skin_mean_b = 0 391 _igv1_set_axis(axes, 0, NX_IGV1_AX_SKIN_TONE_ITA, 392 NX_IGV1_AXIS_NO_SKIN_DETECTED, 0, 0) 393 _igv1_set_axis(axes, 1, NX_IGV1_AX_UNDERTONE, 394 NX_IGV1_AXIS_NO_SKIN_DETECTED, 0, 0) 395 } else { 396 r.skin_mean_r = sum_sr / n_skin 397 r.skin_mean_g = sum_sg / n_skin 398 r.skin_mean_b = sum_sb / n_skin 399 // Lab on SKIN-ONLY mean (the v1 promotion). 400 let lab: *NxLabColor = nx_lab_from_rgb(r.skin_mean_r, r.skin_mean_g, r.skin_mean_b) 401 let ita_r: *NxSkinToneIta = nx_skin_tone_ita_compute( 402 lab.l_q10, lab.a_q10, lab.b_q10) 403 _igv1_set_axis(axes, 0, NX_IGV1_AX_SKIN_TONE_ITA, 404 NX_IGV1_AXIS_OK, ita_r.band, ita_r.ita_deg_q10) 405 let und_r: *NxUndertoneResult = nx_undertone_classify( 406 lab.a_q10, lab.b_q10) 407 _igv1_set_axis(axes, 1, NX_IGV1_AX_UNDERTONE, 408 NX_IGV1_AXIS_OK, und_r.verdict, und_r.conf_q10) 409 } 410 411 // Whole-image luma + skin coverage. 412 let mean_luma: nx_int = (306 * r.mean_r + 601 * r.mean_g + 117 * r.mean_b) / NX_MAGIC_1024 413 _igv1_set_axis(axes, 2, NX_IGV1_AX_LUMINANCE, 414 NX_IGV1_AXIS_OK, 0, mean_luma * 4) 415 _igv1_set_axis(axes, 3, NX_IGV1_AX_SKIN_COVERAGE, 416 NX_IGV1_AXIS_OK, 0, r.skin_coverage_q10) 417 418 // Dimensions. 419 _igv1_set_axis(axes, 4, NX_IGV1_AX_DIMENSIONS, 420 NX_IGV1_AXIS_OK, 0, w * h) 421 422 // Landmark-pending axes (still need face / hand keypoint detection). 423 _igv1_set_axis(axes, 5, NX_IGV1_AX_GLCM_TEXTURE, 424 NX_IGV1_AXIS_PENDING_LANDMARK, 0, 0) 425 _igv1_set_axis(axes, 6, NX_IGV1_AX_PORE_DETAIL, 426 NX_IGV1_AXIS_PENDING_LANDMARK, 0, 0) 427 _igv1_set_axis(axes, 7, NX_IGV1_AX_VEIN_SIGNAL, 428 NX_IGV1_AXIS_PENDING_LANDMARK, 0, 0) 429 _igv1_set_axis(axes, 8, NX_IGV1_AX_SUBSURFACE_SIGNAL, 430 NX_IGV1_AXIS_PENDING_LANDMARK, 0, 0) 431 _igv1_set_axis(axes, 9, NX_IGV1_AX_MICRO_EXPRESSION, 432 NX_IGV1_AXIS_PENDING_LANDMARK, 0, 0) 433 _igv1_set_axis(axes, 10, NX_IGV1_AX_SHADOW_DIRECTION, 434 NX_IGV1_AXIS_PENDING_LANDMARK, 0, 0) 435 _igv1_set_axis(axes, 11, NX_IGV1_AX_FINGER_COUNT, 436 NX_IGV1_AXIS_PENDING_LANDMARK, 0, 0) 437 _igv1_set_axis(axes, 12, NX_IGV1_AX_SURFACE_PLANE, 438 NX_IGV1_AXIS_PENDING_LANDMARK, 0, 0) 439 return r 440} 441 442// ===== self-test ================================================= 443 444func main() -> nx_int { 445 // ---- Kovac skin pixel rule ---- 446 // (200, 170, 150): R>95, G>40, B>20; max-min = 50 > 15; |R-G|=30 > 15; R>G, R>B. 447 if _igv1_is_skin_pixel(200, 170, 150) != 1 { return 1 } 448 // (50, 50, 50): R <= 95 -> not skin 449 if _igv1_is_skin_pixel(50, 50, 50) != 0 { return 2 } 450 // (100, 100, 100): R-G = 0, R not > G -> not skin 451 if _igv1_is_skin_pixel(100, 100, 100) != 0 { return 3 } 452 // (255, 0, 0) pure red: G <= 40 -> not skin 453 if _igv1_is_skin_pixel(255, 0, 0) != 0 { return 4 } 454 // (75, 60, 45) dark skin: R = 75 not > 95 -> NOT classified by Kovac 455 // (the rule is calibrated for lighter tones; dim-light variant queued). 456 if _igv1_is_skin_pixel(75, 60, 45) != 0 { return 5 } 457 // (180, 130, 100) tan skin: passes all rules 458 if _igv1_is_skin_pixel(180, 130, 100) != 1 { return 6 } 459 460 // ---- format detect (sanity) ---- 461 let pm: *u8 = (sys_mmap(4)) as *u8 462 pm[0] = 137 as u8 463 pm[1] = 80 as u8 464 pm[2] = 78 as u8 465 pm[3] = 71 as u8 466 if _igv1_detect_format(pm, 4) != NX_IGV1_FORMAT_PNG { return 10 } 467 468 let jm: *u8 = (sys_mmap(4)) as *u8 469 jm[0] = 255 as u8 470 jm[1] = 216 as u8 471 if _igv1_detect_format(jm, 4) != NX_IGV1_FORMAT_JPEG { return 11 } 472 473 // ---- TOO_SHORT path ---- 474 let r_short: *NxIgv1Report = nx_image_grade_v1(pm, 4) 475 if r_short.error_code != NX_IGV1_ERR_TOO_SHORT { return 20 } 476 477 // ---- UNKNOWN_FORMAT path ---- 478 let bad: *u8 = (sys_mmap(8)) as *u8 479 var i: nx_int = 0 480 while i < 8 { 481 bad[i] = 0 as u8 482 i = i + 1 483 } 484 let r_unk: *NxIgv1Report = nx_image_grade_v1(bad, 8) 485 if r_unk.error_code != NX_IGV1_ERR_UNKNOWN_FORMAT { return 21 } 486 487 return 0 488}