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}