code wiki / (root) / nx_color_v2.nx

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}