code wiki / (root) / nx_light.nx

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}