nx_color_v2.nx source
↩ module page · 311 lines · 9920 B
1// nx_color_v2.nx -- full color-space transforms (V1 of vision rollout).
2//
3// Per VISION_FROM_BITS_UP doc, V1 (color) closes ~50% of the gap to
4// classical scene understanding. Ships:
5//
6// - RGB -> HSV (Hue 0..360 degrees, Sat/Val 0..255)
7// - RGB -> YCbCr (Rec.601 broadcast standard)
8// - K-means palette extraction (3D RGB clustering)
9// - Skin-mask via Kovac 2003 fixed-rule YCbCr classifier
10// - Dominant-channel detection
11//
12// All i64. Q-format scaling for fractional intermediates. No f64.
13//
14// genealogy_id: smith_1978_hsv + itu_r_bt601_ycbcr + kovac_2003_skin
15// + lloyd_1957_kmeans
16// lineage_id: color_space_transform + fixed_point_arithmetic
17// axioms: NX_AX_ALG_DISTRIBUTIVITY (linear transforms compose)
18
19// nx_safety_envelope:
20// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
21// sil_target: SIL1
22// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
23// verdict: NOT_YET_EVALUATED
24
25import "syscalls.nx"
26import "nx_axioms.nx"
27import "nx_i128.nx"
28import "nx_image.nx"
29
30// ===== RGB to HSV ======================================================
31//
32// Given r, g, b in 0..255:
33// max = max(r, g, b)
34// min = min(r, g, b)
35// V = max
36// S = (max - min) * 255 / max (0 if max == 0)
37// H = depends on which is max:
38// if max == r: H = 60 * (g - b) / (max - min) + offset
39// if max == g: H = 60 * (b - r) / (max - min) + 120
40// if max == b: H = 60 * (r - g) / (max - min) + 240
41// normalized to [0, 360).
42//
43// We return H in degrees (0..359), S in 0..255, V in 0..255.
44
45struct HSVColor {
46 h: i64, // hue 0..359
47 s: i64, // saturation 0..255
48 v: i64, // value 0..255
49}
50
51func nx_color_max3(a: i64, b: i64, c: i64) -> i64 {
52 var m: i64 = a
53 if b > m { m = b }
54 if c > m { m = c }
55 return m
56}
57
58func nx_color_min3(a: i64, b: i64, c: i64) -> i64 {
59 var m: i64 = a
60 if b < m { m = b }
61 if c < m { m = c }
62 return m
63}
64
65func nx_color_rgb_to_hsv(r: i64, g: i64, b: i64, out: *HSVColor) -> i64 {
66 let max_v: i64 = nx_color_max3(r, g, b)
67 let min_v: i64 = nx_color_min3(r, g, b)
68 let delta: i64 = max_v - min_v
69 out.v = max_v
70 if max_v == 0 {
71 out.s = 0
72 out.h = 0
73 return 0
74 }
75 out.s = (delta * 255) / max_v
76 if delta == 0 {
77 out.h = 0
78 return 0
79 }
80 var h: i64 = 0
81 if max_v == r {
82 h = 60 * (g - b) / delta
83 }
84 if max_v == g {
85 if max_v != r {
86 h = 60 * (b - r) / delta + 120
87 }
88 }
89 if max_v == b {
90 if max_v != r {
91 if max_v != g {
92 h = 60 * (r - g) / delta + 240
93 }
94 }
95 }
96 while h < 0 { h = h + 360 }
97 while h >= 360 { h = h - 360 }
98 out.h = h
99 return 0
100}
101
102// ===== RGB to YCbCr (Rec.601) ==========================================
103//
104// Y = (66*R + 129*G + 25*B + 128) >> 8 + 16
105// Cb = (-38*R - 74*G + 112*B + 128) >> 8 + 128
106// Cr = (112*R - 94*G - 18*B + 128) >> 8 + 128
107//
108// Output: Y, Cb, Cr in 0..255 each.
109
110struct YCbCrColor {
111 y: i64,
112 cb: i64,
113 cr: i64,
114}
115
116func nx_color_rgb_to_ycbcr(r: i64, g: i64, b: i64, out: *YCbCrColor) -> i64 {
117 let y_raw: i64 = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16
118 let cb_raw: i64 = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128
119 let cr_raw: i64 = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128
120 var y: i64 = y_raw
121 if y < 0 { y = 0 }
122 if y > 255 { y = 255 }
123 var cb: i64 = cb_raw
124 if cb < 0 { cb = 0 }
125 if cb > 255 { cb = 255 }
126 var cr: i64 = cr_raw
127 if cr < 0 { cr = 0 }
128 if cr > 255 { cr = 255 }
129 out.y = y
130 out.cb = cb
131 out.cr = cr
132 return 0
133}
134
135// ===== Kovac 2003 skin-mask classifier =================================
136//
137// Kovac et al. 2003 "Human skin colour clustering for face detection"
138// proposed two fixed-rule classifiers. We use their YCbCr rule:
139//
140// skin pixel iff:
141// Y > 80
142// AND 85 < Cb < 135
143// AND 135 < Cr < 180
144//
145// Pure rule, no training, no learning. Detects skin candidate
146// regions; user then applies morphological cleanup + region analysis.
147//
148// Substrate guarantees: only objective image-property classification.
149// Does not identify individuals; does not rank persons; does not
150// store any reference faces.
151
152func nx_color_kovac_skin_pixel(r: i64, g: i64, b: i64) -> i64 {
153 let ycc: *YCbCrColor = (sys_mmap(24)) as *YCbCrColor
154 nx_color_rgb_to_ycbcr(r, g, b, ycc)
155 if ycc.y <= 80 { return 0 }
156 if ycc.cb <= 85 { return 0 }
157 if ycc.cb >= 135 { return 0 }
158 if ycc.cr <= 135 { return 0 }
159 if ycc.cr >= 180 { return 0 }
160 return 1
161}
162
163// Apply Kovac classifier to a whole RGB image, returning a binary mask.
164func nx_color_skin_mask(rgb: *Image) -> *Image {
165 let mask: *Image = nx_image_alloc(rgb.width, rgb.height, 1)
166 var y: i64 = 0
167 while y < rgb.height {
168 var x: i64 = 0
169 while x < rgb.width {
170 let r: i64 = nx_image_get(rgb, x, y, 0)
171 let g: i64 = nx_image_get(rgb, x, y, 1)
172 let b: i64 = nx_image_get(rgb, x, y, 2)
173 var m: i64 = 0
174 if nx_color_kovac_skin_pixel(r, g, b) == 1 { m = 255 }
175 nx_image_set(mask, x, y, 0, m)
176 x = x + 1
177 }
178 y = y + 1
179 }
180 return mask
181}
182
183// ===== K-means palette extraction (3D RGB) ==============================
184//
185// Given an image and k=number of clusters, return k RGB centroids
186// that approximate the image's color palette.
187//
188// Implementation: Lloyd's algorithm (1957). Random init using image-
189// pixel sampling at fixed strides (deterministic since the sampling
190// stride is fixed). Iterate cluster assignments + centroid update
191// for up to max_iter rounds or until stable.
192
193const NX_COLOR_KMEANS_MAX_K: i64 = 16
194
195struct Palette {
196 k: i64,
197 centroids_r: *i64, // length k
198 centroids_g: *i64,
199 centroids_b: *i64,
200 counts: *i64, // pixels per cluster
201}
202
203func nx_palette_alloc(k: i64) -> *Palette {
204 let raw: *u8 = sys_mmap(40)
205 let p: *Palette = raw as *Palette
206 p.k = k
207 p.centroids_r = (sys_mmap(k * 8)) as *i64
208 p.centroids_g = (sys_mmap(k * 8)) as *i64
209 p.centroids_b = (sys_mmap(k * 8)) as *i64
210 p.counts = (sys_mmap(k * 8)) as *i64
211 return p
212}
213
214// Distance-squared between two RGB colors (i64 safe; max = 3 * 255^2).
215func nx_color_dist_sq(r1: i64, g1: i64, b1: i64,
216 r2: i64, g2: i64, b2: i64) -> i64 {
217 let dr: i64 = r1 - r2
218 let dg: i64 = g1 - g2
219 let db: i64 = b1 - b2
220 return dr * dr + dg * dg + db * db
221}
222
223func nx_color_kmeans_palette(rgb: *Image, k: i64, max_iter: i64) -> *Palette {
224 if k <= 0 { return 0 as *Palette }
225 if k > NX_COLOR_KMEANS_MAX_K { return 0 as *Palette }
226 let p: *Palette = nx_palette_alloc(k)
227 let n_pixels: i64 = rgb.width * rgb.height
228
229 // Initialize centroids by sampling pixels at evenly-spaced positions.
230 var i: i64 = 0
231 while i < k {
232 let idx: i64 = (n_pixels * i) / k
233 let py: i64 = idx / rgb.width
234 let px: i64 = idx - py * rgb.width
235 p.centroids_r[i] = nx_image_get(rgb, px, py, 0)
236 p.centroids_g[i] = nx_image_get(rgb, px, py, 1)
237 p.centroids_b[i] = nx_image_get(rgb, px, py, 2)
238 i = i + 1
239 }
240
241 // Iterate.
242 var iter: i64 = 0
243 while iter < max_iter {
244 // Reset accumulators.
245 let sum_r: *i64 = (sys_mmap(k * 8)) as *i64
246 let sum_g: *i64 = (sys_mmap(k * 8)) as *i64
247 let sum_b: *i64 = (sys_mmap(k * 8)) as *i64
248 var j: i64 = 0
249 while j < k {
250 sum_r[j] = 0
251 sum_g[j] = 0
252 sum_b[j] = 0
253 p.counts[j] = 0
254 j = j + 1
255 }
256 // Assign each pixel to closest centroid.
257 var y: i64 = 0
258 while y < rgb.height {
259 var x: i64 = 0
260 while x < rgb.width {
261 let pr: i64 = nx_image_get(rgb, x, y, 0)
262 let pg: i64 = nx_image_get(rgb, x, y, 1)
263 let pb: i64 = nx_image_get(rgb, x, y, 2)
264 var best_i: i64 = 0
265 var best_d: i64 = nx_color_dist_sq(pr, pg, pb,
266 p.centroids_r[0], p.centroids_g[0], p.centroids_b[0])
267 var ci: i64 = 1
268 while ci < k {
269 let d: i64 = nx_color_dist_sq(pr, pg, pb,
270 p.centroids_r[ci], p.centroids_g[ci], p.centroids_b[ci])
271 if d < best_d { best_d = d; best_i = ci }
272 ci = ci + 1
273 }
274 sum_r[best_i] = sum_r[best_i] + pr
275 sum_g[best_i] = sum_g[best_i] + pg
276 sum_b[best_i] = sum_b[best_i] + pb
277 p.counts[best_i] = p.counts[best_i] + 1
278 x = x + 1
279 }
280 y = y + 1
281 }
282 // Update centroids.
283 j = 0
284 while j < k {
285 if p.counts[j] > 0 {
286 p.centroids_r[j] = sum_r[j] / p.counts[j]
287 p.centroids_g[j] = sum_g[j] / p.counts[j]
288 p.centroids_b[j] = sum_b[j] / p.counts[j]
289 }
290 j = j + 1
291 }
292 iter = iter + 1
293 }
294 return p
295}
296
297// ===== dominant channel detection ======================================
298//
299// Returns 0 if R is dominant, 1 if G, 2 if B, 3 if approximately
300// gray (max - min < threshold).
301
302const NX_COLOR_GRAY_THRESHOLD: i64 = 20
303
304func nx_color_dominant_channel(r: i64, g: i64, b: i64) -> i64 {
305 let max_v: i64 = nx_color_max3(r, g, b)
306 let min_v: i64 = nx_color_min3(r, g, b)
307 if max_v - min_v < NX_COLOR_GRAY_THRESHOLD { return 3 }
308 if max_v == r { return 0 }
309 if max_v == g { return 1 }
310 return 2
311}