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}