code wiki / (root) / nx_specular_falloff.nx

nx_specular_falloff.nx source

↩ module page · 144 lines · 4817 B

1// nx_specular_falloff.nx -- measure highlight sharpness (broad=real skin, sharp=plastic). 2// 3// Real skin has wide soft specular highlights (Oren-Nayar / subsurface 4// micro-roughness). Mannequin / plastic renders have narrow tight 5// highlights (sharp Phong-like). The substrate measures the spatial 6// FWHM (full width at half max) of the brightest pixel cluster within 7// the skin mask. 8// 9// Math: 10// 1. Find brightest pixel within skin mask. 11// 2. Measure how many pixels around it have luminance >= half_max. 12// 3. FWHM = sqrt(area_above_half_max). 13// 4. Score Q10: broad FWHM = natural; narrow FWHM = plastic. 14// 15// Output flat-array: 16// result[0] = peak_x 17// result[1] = peak_y 18// result[2] = peak_luminance 19// result[3] = fwhm_pixels (sqrt of area where Y >= peak/2) 20// result[4] = falloff_score_q10 (1024=very broad/natural; 0=very sharp/plastic) 21// result[5] = verdict (0=PLASTIC, 1=AMBIGUOUS, 2=NATURAL) 22 23// nx_safety_envelope: 24// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 25// sil_target: SIL1 26// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 27// verdict: NOT_YET_EVALUATED 28 29import "nx_syscalls.nx" 30import "nx_image.nx" 31 32const NX_SPEC_RES_PEAK_X: i64 = 0 33const NX_SPEC_RES_PEAK_Y: i64 = 1 34const NX_SPEC_RES_PEAK_Y_LUM: i64 = 2 35const NX_SPEC_RES_FWHM: i64 = 3 36const NX_SPEC_RES_SCORE: i64 = 4 37const NX_SPEC_RES_VERDICT: i64 = 5 38const NX_SPEC_RES_FIELDS: i64 = 6 39 40const NX_SPEC_VERDICT_PLASTIC: i64 = 0 41const NX_SPEC_VERDICT_AMBIGUOUS: i64 = 1 42const NX_SPEC_VERDICT_NATURAL: i64 = 2 43 44// Threshold FWHM relative to image diagonal. Natural skin: FWHM > 4% of diag. 45const NX_SPEC_FWHM_PLASTIC_Q10: i64 = 20 // 0.02 of diag 46const NX_SPEC_FWHM_NATURAL_Q10: i64 = 40 // 0.04 of diag 47 48// Integer sqrt (Newton). 49func nx_spec_isqrt(x: i64) -> i64 { 50 if x <= 0 { return 0 } 51 var r: i64 = x 52 var i: i64 = 0 53 while i < 24 { 54 let next: i64 = (r + x / r) / 2 55 if next >= r { return r } 56 r = next 57 i = i + 1 58 } 59 return r 60} 61 62func nx_specular_falloff(rgb: *Image, mask: *Image, result: *i64) -> i64 { 63 var i: i64 = 0 64 while i < NX_SPEC_RES_FIELDS { result[i] = 0; i = i + 1 } 65 let w: i64 = rgb.width 66 let h: i64 = rgb.height 67 68 // 1st pass: find brightest skin-pixel. 69 var peak_x: i64 = -1 70 var peak_y: i64 = -1 71 var peak_lum: i64 = 0 72 var y: i64 = 0 73 while y < h { 74 var x: i64 = 0 75 while x < w { 76 let m: i64 = nx_image_get(mask, x, y, 0) 77 if m > 0 { 78 let r: i64 = nx_image_get(rgb, x, y, 0) 79 let g: i64 = nx_image_get(rgb, x, y, 1) 80 let bch: i64 = nx_image_get(rgb, x, y, 2) 81 let yv: i64 = (77 * r + 150 * g + 29 * bch) / 256 82 if yv > peak_lum { 83 peak_lum = yv 84 peak_x = x 85 peak_y = y 86 } 87 } 88 x = x + 1 89 } 90 y = y + 1 91 } 92 if peak_x < 0 { return 0 } 93 result[NX_SPEC_RES_PEAK_X] = peak_x 94 result[NX_SPEC_RES_PEAK_Y] = peak_y 95 result[NX_SPEC_RES_PEAK_Y_LUM] = peak_lum 96 97 let half_max: i64 = peak_lum / 2 98 var area_above: i64 = 0 99 // 2nd pass: count skin-mask pixels with Y >= half_max. 100 y = 0 101 while y < h { 102 var x: i64 = 0 103 while x < w { 104 let m: i64 = nx_image_get(mask, x, y, 0) 105 if m > 0 { 106 let r: i64 = nx_image_get(rgb, x, y, 0) 107 let g: i64 = nx_image_get(rgb, x, y, 1) 108 let bch: i64 = nx_image_get(rgb, x, y, 2) 109 let yv: i64 = (77 * r + 150 * g + 29 * bch) / 256 110 if yv >= half_max { area_above = area_above + 1 } 111 } 112 x = x + 1 113 } 114 y = y + 1 115 } 116 let fwhm: i64 = nx_spec_isqrt(area_above) 117 result[NX_SPEC_RES_FWHM] = fwhm 118 119 // Normalize FWHM to Q10 fraction of image diagonal. 120 let diag: i64 = nx_spec_isqrt(w * w + h * h) 121 var fwhm_q10: i64 = 0 122 if diag > 0 { fwhm_q10 = (fwhm * 1024) / diag } 123 124 // Score: larger fraction = more natural. 125 var score: i64 = 0 126 if fwhm_q10 >= NX_SPEC_FWHM_NATURAL_Q10 { 127 score = 1024 128 } else { 129 if fwhm_q10 > NX_SPEC_FWHM_PLASTIC_Q10 { 130 score = ((fwhm_q10 - NX_SPEC_FWHM_PLASTIC_Q10) * 1024) / 131 (NX_SPEC_FWHM_NATURAL_Q10 - NX_SPEC_FWHM_PLASTIC_Q10) 132 } 133 } 134 result[NX_SPEC_RES_SCORE] = score 135 136 var verdict: i64 = NX_SPEC_VERDICT_PLASTIC 137 if fwhm_q10 >= NX_SPEC_FWHM_NATURAL_Q10 { 138 verdict = NX_SPEC_VERDICT_NATURAL 139 } else { 140 if fwhm_q10 > NX_SPEC_FWHM_PLASTIC_Q10 { verdict = NX_SPEC_VERDICT_AMBIGUOUS } 141 } 142 result[NX_SPEC_RES_VERDICT] = verdict 143 return 0 144}