nx_light.nx source
↩ module page · 233 lines · 7012 B
1// nx_light.nx -- exposure / contrast / dynamic-range / direction (V3).
2//
3// Pure-math image-quality and lighting analysis. All i64.
4//
5// Ships:
6// - nx_light_mean_luminance: average grayscale value
7// - nx_light_stddev_luminance: sqrt(variance), integer
8// - nx_light_min / max: tonal extrema
9// - nx_light_dynamic_range: max - min
10// - nx_light_underexposed_frac: Q10 fraction below threshold
11// - nx_light_overexposed_frac: Q10 fraction above threshold
12// - nx_light_direction_classify: bin a single (gx, gy) into 8 octants
13// - nx_light_direction_hist: gradient-direction histogram
14//
15// Octant numbering (compass-like, y-down image convention):
16// 0 = E (gradient points right; bright on right)
17// 1 = SE (right-down)
18// 2 = S (down)
19// 3 = SW
20// 4 = W
21// 5 = NW
22// 6 = N (up)
23// 7 = NE
24//
25// genealogy_id: ansel_adams_zone_system + datta_2006_features
26// lineage_id: luminance_statistics + gradient_direction_quantization
27
28// nx_safety_envelope:
29// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
30// sil_target: SIL1
31// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
32// verdict: NOT_YET_EVALUATED
33
34import "syscalls.nx"
35import "nx_image.nx"
36import "nx_math.nx"
37
38const NX_LIGHT_Q: i64 = 1024
39const NX_LIGHT_DIRS: i64 = 8
40
41// Integer sqrt is canonical in nx_math.nx.
42
43// ===== Luminance statistics =============================================
44
45func nx_light_mean_luminance(gray: *Image) -> i64 {
46 let w: i64 = gray.width
47 let h: i64 = gray.height
48 let n: i64 = w * h
49 if n == 0 { return 0 }
50 var sum: i64 = 0
51 var y: i64 = 0
52 while y < h {
53 var x: i64 = 0
54 while x < w {
55 sum = sum + nx_image_get(gray, x, y, 0)
56 x = x + 1
57 }
58 y = y + 1
59 }
60 return sum / n
61}
62
63// Stddev given mean (caller supplies mean to avoid double-pass when
64// caller already has it).
65func nx_light_stddev_luminance(gray: *Image, mean: i64) -> i64 {
66 let w: i64 = gray.width
67 let h: i64 = gray.height
68 let n: i64 = w * h
69 if n == 0 { return 0 }
70 var var_sum: i64 = 0
71 var y: i64 = 0
72 while y < h {
73 var x: i64 = 0
74 while x < w {
75 let v: i64 = nx_image_get(gray, x, y, 0)
76 let d: i64 = v - mean
77 var_sum = var_sum + d * d
78 x = x + 1
79 }
80 y = y + 1
81 }
82 return nx_math_isqrt(var_sum / n)
83}
84
85func nx_light_min(gray: *Image) -> i64 {
86 let w: i64 = gray.width
87 let h: i64 = gray.height
88 var m: i64 = 255
89 var y: i64 = 0
90 while y < h {
91 var x: i64 = 0
92 while x < w {
93 let v: i64 = nx_image_get(gray, x, y, 0)
94 if v < m { m = v }
95 x = x + 1
96 }
97 y = y + 1
98 }
99 return m
100}
101
102func nx_light_max(gray: *Image) -> i64 {
103 let w: i64 = gray.width
104 let h: i64 = gray.height
105 var m: i64 = 0
106 var y: i64 = 0
107 while y < h {
108 var x: i64 = 0
109 while x < w {
110 let v: i64 = nx_image_get(gray, x, y, 0)
111 if v > m { m = v }
112 x = x + 1
113 }
114 y = y + 1
115 }
116 return m
117}
118
119func nx_light_dynamic_range(gray: *Image) -> i64 {
120 return nx_light_max(gray) - nx_light_min(gray)
121}
122
123// ===== Exposure fractions (Q10) =========================================
124
125func nx_light_underexposed_frac(gray: *Image, threshold: i64) -> i64 {
126 let w: i64 = gray.width
127 let h: i64 = gray.height
128 let n: i64 = w * h
129 if n == 0 { return 0 }
130 var count: i64 = 0
131 var y: i64 = 0
132 while y < h {
133 var x: i64 = 0
134 while x < w {
135 if nx_image_get(gray, x, y, 0) < threshold { count = count + 1 }
136 x = x + 1
137 }
138 y = y + 1
139 }
140 return (count * NX_LIGHT_Q) / n
141}
142
143func nx_light_overexposed_frac(gray: *Image, threshold: i64) -> i64 {
144 let w: i64 = gray.width
145 let h: i64 = gray.height
146 let n: i64 = w * h
147 if n == 0 { return 0 }
148 var count: i64 = 0
149 var y: i64 = 0
150 while y < h {
151 var x: i64 = 0
152 while x < w {
153 if nx_image_get(gray, x, y, 0) > threshold { count = count + 1 }
154 x = x + 1
155 }
156 y = y + 1
157 }
158 return (count * NX_LIGHT_Q) / n
159}
160
161// ===== Gradient direction classification (8 octants) ====================
162//
163// Avoids arctan2 entirely. Uses sign + magnitude-ratio tests to bin
164// (gx, gy) into one of 8 octants. Returns -1 if magnitude is zero.
165
166func nx_light_direction_classify(gx: i64, gy: i64) -> i64 {
167 if gx == 0 { if gy == 0 { return -1 } }
168 var ax: i64 = gx
169 if ax < 0 { ax = -ax }
170 var ay: i64 = gy
171 if ay < 0 { ay = -ay }
172 // First decide if axis-dominant (|gx| > 2|gy| or |gy| > 2|gx|).
173 // Sentinels for "diagonal" zone.
174 let x_dom: i64 = ax * 5 / 12 // tan(22.5 deg) ~ 0.414
175 let y_dom: i64 = ay * 5 / 12
176 // Use a simpler split: axis-dominant if ay <= ax/2 (similar effect).
177 // For 8 even octants, the proper boundary is at tan(22.5). We use
178 // the approximation ay * 2 < ax for "more horizontal than diagonal".
179 var horizontal: i64 = 0
180 var vertical: i64 = 0
181 if ay * 2 < ax { horizontal = 1 }
182 if ax * 2 < ay { vertical = 1 }
183 if horizontal == 1 {
184 if gx >= 0 { return 0 } // E
185 return 4 // W
186 }
187 if vertical == 1 {
188 if gy >= 0 { return 2 } // S (y-down)
189 return 6 // N
190 }
191 // Diagonal cases (neither strongly horizontal nor vertical).
192 if gx >= 0 {
193 if gy >= 0 { return 1 } // SE
194 return 7 // NE
195 }
196 if gy >= 0 { return 3 } // SW
197 return 5 // NW
198}
199
200// Histogram of gradient direction over the image. Only pixels with
201// magnitude^2 >= mag2_threshold contribute (filters noise).
202// hist must point to 8 i64 slots.
203
204func nx_light_direction_hist(gx_img: *ImageS64, gy_img: *ImageS64,
205 mag2_threshold: i64, hist: *i64) -> i64 {
206 var i: i64 = 0
207 while i < NX_LIGHT_DIRS {
208 hist[i] = 0
209 i = i + 1
210 }
211 let w: i64 = gx_img.width
212 let h: i64 = gx_img.height
213 var y: i64 = 0
214 while y < h {
215 var x: i64 = 0
216 while x < w {
217 let gx: i64 = nx_image_s64_get(gx_img, x, y)
218 let gy: i64 = nx_image_s64_get(gy_img, x, y)
219 let mag2: i64 = gx * gx + gy * gy
220 if mag2 >= mag2_threshold {
221 let d: i64 = nx_light_direction_classify(gx, gy)
222 if d >= 0 {
223 if d < NX_LIGHT_DIRS {
224 hist[d] = hist[d] + 1
225 }
226 }
227 }
228 x = x + 1
229 }
230 y = y + 1
231 }
232 return 0
233}