nx_subsurface_warmth.nx source
↩ module page · 155 lines · 5325 B
1// nx_subsurface_warmth.nx -- detect subsurface scatter (SSS) by R/G ratio in shadows.
2//
3// Real skin transmits red wavelength deeper than green/blue. In shadow
4// regions (low overall luminance), real skin shows ELEVATED red-to-green
5// ratio relative to the lit side. Mannequin renders / SDXL plastic
6// renders show flat R:G ratio across light and shadow.
7//
8// Math:
9// 1. Pick skin-mask pixels with luminance below median (shadow side).
10// 2. Compute mean R, mean G across those pixels.
11// 3. Mean R / Mean G in Q10. SSS-positive when > 1100 (≈1.075).
12// 4. Score Q10: how far above the SSS threshold.
13//
14// Output flat-array (result[]):
15// result[0] = shadow_pixel_count
16// result[1] = mean_R_q10 (mean R*4 to keep precision)
17// result[2] = mean_G_q10
18// result[3] = R_to_G_q10 (ratio in Q10)
19// result[4] = sss_score_q10 (0=no SSS, 1024=strong SSS)
20// result[5] = verdict (0=MANNEQUIN, 1=SLIGHT, 2=NATURAL)
21
22// nx_safety_envelope:
23// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
24// sil_target: SIL1
25// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
26// verdict: NOT_YET_EVALUATED
27
28import "nx_syscalls.nx"
29import "nx_image.nx"
30
31const NX_SSS_RES_COUNT: i64 = 0
32const NX_SSS_RES_MEAN_R: i64 = 1
33const NX_SSS_RES_MEAN_G: i64 = 2
34const NX_SSS_RES_RATIO: i64 = 3
35const NX_SSS_RES_SCORE: i64 = 4
36const NX_SSS_RES_VERDICT: i64 = 5
37const NX_SSS_RES_FIELDS: i64 = 6
38
39const NX_SSS_VERDICT_MANNEQUIN: i64 = 0 // ratio ≈ 1.0 (flat)
40const NX_SSS_VERDICT_SLIGHT: i64 = 1 // 1.05..1.075
41const NX_SSS_VERDICT_NATURAL: i64 = 2 // > 1.075
42
43const NX_SSS_THRESH_NATURAL_Q10: i64 = 1100 // 1.075 in Q10
44const NX_SSS_THRESH_SLIGHT_Q10: i64 = 1075 // 1.05 in Q10
45
46// Compute luminance threshold via simple percentile estimate over mask.
47// Returns the median Y over skin-mask pixels (or 128 fallback).
48func nx_sss_luminance_median(rgb: *Image, mask: *Image) -> i64 {
49 let h: i64 = mask.height
50 let w: i64 = mask.width
51 // 32-bin luminance histogram over skin pixels.
52 let hist: *i64 = (sys_mmap(32 * 8 + 16)) as *i64
53 var b: i64 = 0
54 while b < 32 { hist[b] = 0; b = b + 1 }
55 var n: i64 = 0
56 var y: i64 = 0
57 while y < h {
58 var x: i64 = 0
59 while x < w {
60 let m: i64 = nx_image_get(mask, x, y, 0)
61 if m > 0 {
62 let r: i64 = nx_image_get(rgb, x, y, 0)
63 let g: i64 = nx_image_get(rgb, x, y, 1)
64 let bch: i64 = nx_image_get(rgb, x, y, 2)
65 let yval: i64 = (77 * r + 150 * g + 29 * bch) / 256
66 var bin: i64 = yval / 8
67 if bin > 31 { bin = 31 }
68 hist[bin] = hist[bin] + 1
69 n = n + 1
70 }
71 x = x + 1
72 }
73 y = y + 1
74 }
75 if n == 0 { return 128 }
76 let half: i64 = n / 2
77 var acc: i64 = 0
78 var i: i64 = 0
79 while i < 32 {
80 acc = acc + hist[i]
81 if acc >= half { return i * 8 + 4 }
82 i = i + 1
83 }
84 return 128
85}
86
87// Run SSS analysis. Caller provides RGB image + skin mask + result array.
88func nx_subsurface_warmth(rgb: *Image, mask: *Image, result: *i64) -> i64 {
89 // Init result.
90 var i: i64 = 0
91 while i < NX_SSS_RES_FIELDS { result[i] = 0; i = i + 1 }
92
93 let median_y: i64 = nx_sss_luminance_median(rgb, mask)
94 let shadow_thresh: i64 = median_y
95
96 var sum_r: i64 = 0
97 var sum_g: i64 = 0
98 var count: i64 = 0
99 let w: i64 = rgb.width
100 let h: i64 = rgb.height
101 var y: i64 = 0
102 while y < h {
103 var x: i64 = 0
104 while x < w {
105 let m: i64 = nx_image_get(mask, x, y, 0)
106 if m > 0 {
107 let r: i64 = nx_image_get(rgb, x, y, 0)
108 let g: i64 = nx_image_get(rgb, x, y, 1)
109 let bch: i64 = nx_image_get(rgb, x, y, 2)
110 let yval: i64 = (77 * r + 150 * g + 29 * bch) / 256
111 if yval < shadow_thresh {
112 sum_r = sum_r + r
113 sum_g = sum_g + g
114 count = count + 1
115 }
116 }
117 x = x + 1
118 }
119 y = y + 1
120 }
121
122 result[NX_SSS_RES_COUNT] = count
123 if count == 0 { return 0 }
124
125 let mean_r: i64 = sum_r / count
126 let mean_g: i64 = sum_g / count
127 result[NX_SSS_RES_MEAN_R] = mean_r
128 result[NX_SSS_RES_MEAN_G] = mean_g
129 if mean_g == 0 { return 0 }
130
131 let ratio_q10: i64 = (mean_r * 1024) / mean_g
132 result[NX_SSS_RES_RATIO] = ratio_q10
133
134 // Score: linear from threshold_slight up to threshold_natural+spread.
135 var score: i64 = 0
136 let upper: i64 = NX_SSS_THRESH_NATURAL_Q10 + 100
137 if ratio_q10 > NX_SSS_THRESH_SLIGHT_Q10 {
138 if ratio_q10 >= upper {
139 score = 1024
140 } else {
141 score = ((ratio_q10 - NX_SSS_THRESH_SLIGHT_Q10) * 1024) /
142 (upper - NX_SSS_THRESH_SLIGHT_Q10)
143 }
144 }
145 result[NX_SSS_RES_SCORE] = score
146
147 var verdict: i64 = NX_SSS_VERDICT_MANNEQUIN
148 if ratio_q10 >= NX_SSS_THRESH_NATURAL_Q10 {
149 verdict = NX_SSS_VERDICT_NATURAL
150 } else {
151 if ratio_q10 >= NX_SSS_THRESH_SLIGHT_Q10 { verdict = NX_SSS_VERDICT_SLIGHT }
152 }
153 result[NX_SSS_RES_VERDICT] = verdict
154 return 0
155}