code wiki / (root) / nx_image.nx

nx_image.nx source

↩ module page · 359 lines · 11970 B

1// nx_image.nx -- pure-math image primitives. 2// 3// No model files, no learned weights, no VLM. Classical computer 4// vision math: pixel access, grayscale conversion, 3x3 convolution, 5// Sobel gradient, gradient magnitude via integer sqrt, histogram. 6// 7// All i64. Uses nx_i128 muldiv where intermediate products risk 8// overflow (per NUMERIC_TIER_LADDER + S_CLASS_MATH_DOCTRINE). 9// 10// genealogy_id: gonzalez_woods_digital_image_processing + canny_1986 11// + sobel_feldman_1968 12// lineage_id: convolution + gradient + integer_pixel_arithmetic 13// axioms: NX_AX_ALG_DISTRIBUTIVITY, NX_AX_ALG_COMMUTATIVITY 14// 15// nx_safety_envelope: 16// intended_use: "Image container + convolution/gradient/ 17// integer-pixel ops. Foundation for all 18// substrate visual processing." 19// sil_target: SIL2 (visual integrity in safety-relevant 20// UI -- e.g. medical imaging, 21// aviation HUDs) 22// asil_target: QM 23// dal_target: DAL C 24// evidence: [Image_canonical_container_type, 25// bounded_pixel_loops, 26// axiom_distributivity_commutativity_tagged] 27// hazard_register: [bug-tape-image-stride-not-validated, 28// bug-tape-OOB-pixel-read-via-bad-coord, 29// bug-tape-channel-count-mismatch-corruption] 30// residual_risk: "Image stride + dimensions are caller- 31// supplied. Substrate validates bounds at 32// function entry; loop bodies trust them. 33// Image format compatibility (RGB vs RGBA 34// vs YUV) is upstream responsibility." 35// verdict: NOT_YET_EVALUATED 36 37import "nx_syscalls.nx" 38import "nx_axioms.nx" 39import "nx_i128.nx" 40 41// ===== image struct ==================================================== 42// 43// pixels = byte buffer, row-major. For RGB: 3 bytes per pixel. 44// stride = bytes per row (== width * channels for tightly packed). 45 46struct Image { 47 pixels: *u8, 48 width: i64, 49 height: i64, 50 channels: i64, 51 stride: i64, 52} 53 54const NX_IMG_BYTES: i64 = 40 55 56func nx_image_alloc(width: i64, height: i64, channels: i64) -> *Image { 57 let raw: *u8 = sys_mmap(NX_IMG_BYTES) 58 let img: *Image = raw as *Image 59 img.width = width 60 img.height = height 61 img.channels = channels 62 img.stride = width * channels 63 img.pixels = sys_mmap(width * height * channels + 16) 64 return img 65} 66 67// Allocate a 1-channel signed-grayscale image whose values can be 68// negative (e.g., gradient output). We store as i64 array under 69// the hood (width*height*8 bytes). 70struct ImageS64 { 71 data: *i64, 72 width: i64, 73 height: i64, 74} 75 76const NX_IMGS64_BYTES: i64 = 24 77 78func nx_image_s64_alloc(width: i64, height: i64) -> *ImageS64 { 79 let raw: *u8 = sys_mmap(NX_IMGS64_BYTES) 80 let img: *ImageS64 = raw as *ImageS64 81 img.width = width 82 img.height = height 83 img.data = (sys_mmap(width * height * 8 + 16)) as *i64 84 return img 85} 86 87// ===== pixel access (u8 image) ========================================= 88 89func nx_image_get(img: *Image, x: i64, y: i64, c: i64) -> i64 { 90 if x < 0 { return 0 } 91 if y < 0 { return 0 } 92 if x >= img.width { return 0 } 93 if y >= img.height { return 0 } 94 return img.pixels[y * img.stride + x * img.channels + c] 95} 96 97func nx_image_set(img: *Image, x: i64, y: i64, c: i64, v: i64) -> i64 { 98 if x < 0 { return -1 } 99 if y < 0 { return -1 } 100 if x >= img.width { return -1 } 101 if y >= img.height { return -1 } 102 var vc: i64 = v 103 if vc < 0 { vc = 0 } 104 if vc > 255 { vc = 255 } 105 img.pixels[y * img.stride + x * img.channels + c] = vc 106 return 0 107} 108 109// ===== signed-grayscale access ======================================== 110 111func nx_image_s64_get(img: *ImageS64, x: i64, y: i64) -> i64 { 112 if x < 0 { return 0 } 113 if y < 0 { return 0 } 114 if x >= img.width { return 0 } 115 if y >= img.height { return 0 } 116 return img.data[y * img.width + x] 117} 118 119func nx_image_s64_set(img: *ImageS64, x: i64, y: i64, v: i64) -> i64 { 120 if x < 0 { return -1 } 121 if y < 0 { return -1 } 122 if x >= img.width { return -1 } 123 if y >= img.height { return -1 } 124 img.data[y * img.width + x] = v 125 return 0 126} 127 128// ===== grayscale conversion (Rec. 709 weights, scaled) ================= 129// 130// Y = (218 * R + 732 * G + 74 * B) / 1024 131// (approximates 0.2126 + 0.7152 + 0.0722 with 10-bit precision). 132 133func nx_image_to_grayscale(rgb: *Image) -> *Image { 134 let gray: *Image = nx_image_alloc(rgb.width, rgb.height, 1) 135 var y: i64 = 0 136 while y < rgb.height { 137 var x: i64 = 0 138 while x < rgb.width { 139 let r: i64 = nx_image_get(rgb, x, y, 0) 140 let g: i64 = nx_image_get(rgb, x, y, 1) 141 let b: i64 = nx_image_get(rgb, x, y, 2) 142 let lum: i64 = (218 * r + 732 * g + 74 * b) / 1024 143 nx_image_set(gray, x, y, 0, lum) 144 x = x + 1 145 } 146 y = y + 1 147 } 148 return gray 149} 150 151// ===== 3x3 convolution (kernel is i64[9], output as ImageS64) ========== 152// 153// Output: out[y, x] = sum over (kj, ki) of kernel[(kj+1)*3 + (ki+1)] * 154// input[y+kj, x+ki] for kj, ki in {-1, 0, 1}. 155// Border policy: clamp to edge (uses nx_image_get's bounds check). 156 157func nx_image_convolve_3x3(src: *Image, kernel: *i64, channel: i64) -> *ImageS64 { 158 let out: *ImageS64 = nx_image_s64_alloc(src.width, src.height) 159 var y: i64 = 0 160 while y < src.height { 161 var x: i64 = 0 162 while x < src.width { 163 var sum: i64 = 0 164 var kj: i64 = 0 165 while kj < 3 { 166 var ki: i64 = 0 167 while ki < 3 { 168 let yy: i64 = y + kj - 1 169 let xx: i64 = x + ki - 1 170 let px: i64 = nx_image_get(src, xx, yy, channel) 171 sum = sum + kernel[kj * 3 + ki] * px 172 ki = ki + 1 173 } 174 kj = kj + 1 175 } 176 nx_image_s64_set(out, x, y, sum) 177 x = x + 1 178 } 179 y = y + 1 180 } 181 return out 182} 183 184// ===== Sobel gradient ================================================== 185// 186// Sx = [-1 0 1 ; -2 0 2 ; -1 0 1] 187// Sy = [-1 -2 -1 ; 0 0 0 ; 1 2 1] 188 189func nx_image_sobel_x(gray: *Image) -> *ImageS64 { 190 let k: *i64 = (sys_mmap(72)) as *i64 191 k[0] = -1; k[1] = 0; k[2] = 1 192 k[3] = -2; k[4] = 0; k[5] = 2 193 k[6] = -1; k[7] = 0; k[8] = 1 194 return nx_image_convolve_3x3(gray, k, 0) 195} 196 197func nx_image_sobel_y(gray: *Image) -> *ImageS64 { 198 let k: *i64 = (sys_mmap(72)) as *i64 199 k[0] = -1; k[1] = -2; k[2] = -1 200 k[3] = 0; k[4] = 0; k[5] = 0 201 k[6] = 1; k[7] = 2; k[8] = 1 202 return nx_image_convolve_3x3(gray, k, 0) 203} 204 205// Gradient magnitude: sqrt(gx^2 + gy^2) via nx_th_isqrt-style. 206// Uses muldiv to avoid overflow in gx^2 + gy^2. 207func nx_image_gradient_magnitude(gx: *ImageS64, gy: *ImageS64) -> *ImageS64 { 208 let out: *ImageS64 = nx_image_s64_alloc(gx.width, gx.height) 209 var y: i64 = 0 210 while y < gx.height { 211 var x: i64 = 0 212 while x < gx.width { 213 let gxv: i64 = nx_image_s64_get(gx, x, y) 214 let gyv: i64 = nx_image_s64_get(gy, x, y) 215 let s: i64 = nx_muldiv_i64(gxv, gxv, 1) + nx_muldiv_i64(gyv, gyv, 1) 216 // integer sqrt via Newton's method 217 var mag: i64 = 0 218 if s > 0 { 219 var z: i64 = s 220 var w: i64 = (z + 1) / 2 221 while w < z { 222 z = w 223 w = (z + s / z) / 2 224 } 225 mag = z 226 } 227 nx_image_s64_set(out, x, y, mag) 228 x = x + 1 229 } 230 y = y + 1 231 } 232 return out 233} 234 235// ===== histogram ======================================================= 236// 237// 256-bin intensity histogram for a single-channel image. Output is 238// caller-allocated i64[256] filled with counts. 239 240func nx_image_histogram_256(img: *Image, channel: i64, out_bins: *i64) -> i64 { 241 var i: i64 = 0 242 while i < 256 { out_bins[i] = 0; i = i + 1 } 243 var y: i64 = 0 244 while y < img.height { 245 var x: i64 = 0 246 while x < img.width { 247 let v: i64 = nx_image_get(img, x, y, channel) 248 if v >= 0 { 249 if v < 256 { 250 out_bins[v] = out_bins[v] + 1 251 } 252 } 253 x = x + 1 254 } 255 y = y + 1 256 } 257 return 0 258} 259 260// Otsu's method: find the threshold that maximizes between-class 261// variance. Returns threshold in [0, 255]. Pure classical statistics. 262// 263// genealogy_id: otsu_1979 264// lineage_id: histogram + variance + maximization 265// axioms: NX_AX_PROB_NONNEGATIVITY 266 267func nx_image_otsu_threshold(img: *Image, channel: i64) -> i64 { 268 let bins: *i64 = (sys_mmap(256 * 8)) as *i64 269 nx_image_histogram_256(img, channel, bins) 270 var total: i64 = 0 271 var sum_all: i64 = 0 272 var i: i64 = 0 273 while i < 256 { 274 total = total + bins[i] 275 sum_all = sum_all + i * bins[i] 276 i = i + 1 277 } 278 if total == 0 { return 0 } 279 var sum_back: i64 = 0 280 var w_back: i64 = 0 281 var max_var: i64 = -1 282 var best_t: i64 = 0 283 var t: i64 = 0 284 while t < 256 { 285 w_back = w_back + bins[t] 286 if w_back > 0 { 287 let w_fore: i64 = total - w_back 288 if w_fore > 0 { 289 sum_back = sum_back + t * bins[t] 290 let mean_back: i64 = sum_back / w_back 291 let mean_fore: i64 = (sum_all - sum_back) / w_fore 292 let diff: i64 = mean_back - mean_fore 293 let var_between: i64 = nx_muldiv_i64( 294 nx_muldiv_i64(w_back, w_fore, 1), 295 nx_muldiv_i64(diff, diff, 1), 1) 296 if var_between > max_var { 297 max_var = var_between 298 best_t = t 299 } 300 } 301 } 302 t = t + 1 303 } 304 return best_t 305} 306 307// ===== threshold to binary mask ======================================= 308 309func nx_image_threshold(img: *Image, channel: i64, thresh: i64) -> *Image { 310 let mask: *Image = nx_image_alloc(img.width, img.height, 1) 311 var y: i64 = 0 312 while y < img.height { 313 var x: i64 = 0 314 while x < img.width { 315 let v: i64 = nx_image_get(img, x, y, channel) 316 var out: i64 = 0 317 if v >= thresh { out = 255 } 318 nx_image_set(mask, x, y, 0, out) 319 x = x + 1 320 } 321 y = y + 1 322 } 323 return mask 324} 325 326// ===== ImageS64 -> normalized u8 image ================================ 327// 328// Maps the dynamic range of an ImageS64 to [0, 255] for display / 329// further processing. Saturating clamp. 330 331func nx_image_s64_to_u8(src: *ImageS64) -> *Image { 332 let out: *Image = nx_image_alloc(src.width, src.height, 1) 333 // Find min + max. 334 var min_v: i64 = src.data[0] 335 var max_v: i64 = src.data[0] 336 var i: i64 = 0 337 while i < src.width * src.height { 338 let v: i64 = src.data[i] 339 if v < min_v { min_v = v } 340 if v > max_v { max_v = v } 341 i = i + 1 342 } 343 let range: i64 = max_v - min_v 344 if range <= 0 { 345 return out 346 } 347 var y: i64 = 0 348 while y < src.height { 349 var x: i64 = 0 350 while x < src.width { 351 let v: i64 = nx_image_s64_get(src, x, y) 352 let scaled: i64 = nx_muldiv_i64(v - min_v, 255, range) 353 nx_image_set(out, x, y, 0, scaled) 354 x = x + 1 355 } 356 y = y + 1 357 } 358 return out 359}