code wiki / (root) / nx_colorspace.nx

nx_colorspace.nx source

↩ module page · 69 lines · 3219 B

1// nx_colorspace.nx -- sovereign RGB <-> YCbCr (JFIF/BT.601 full-range) + 4:2:0 chroma subsample. The video codec 2// (nx_vmotion/vtransform/ventropy) is per-plane 8bpp; real video is colour, so the sender converts the browser's 3// RGB to Y (full res) + Cb,Cr (half res = 4:2:0, since the eye is far less sensitive to chroma) and codes each 4// plane; the receiver upsamples + converts back. Integer-only (no FPU). The browser only extracts RGB from the 5// canvas (last mile); ALL colour math is here. license_tier: ORIGINAL 6 7func cs_clamp(v: i64) -> i64 { if v < 0 { return 0 } if v > 255 { return 255 } return v } 8 9// per-pixel RGB -> YCbCr (x256 integer coeffs; neutral gray -> Cb=Cr=128). /256 (not >>8) is sign-safe for the 10// negative chroma/inverse intermediates (NishiLang >> sign behavior is unspecified; / truncates toward zero). 11func cs_y(r: i64, g: i64, b: i64) -> i64 { return cs_clamp((77*r + 150*g + 29*b) / 256) } 12func cs_cb(r: i64, g: i64, b: i64) -> i64 { return cs_clamp(((0-43)*r - 85*g + 128*b) / 256 + 128) } 13func cs_cr(r: i64, g: i64, b: i64) -> i64 { return cs_clamp((128*r - 107*g - 21*b) / 256 + 128) } 14// per-pixel YCbCr -> RGB 15func cs_r(y: i64, cb: i64, cr: i64) -> i64 { return cs_clamp(y + (359*(cr-128)) / 256) } 16func cs_g(y: i64, cb: i64, cr: i64) -> i64 { return cs_clamp(y - (88*(cb-128)) / 256 - (183*(cr-128)) / 256) } 17func cs_b(y: i64, cb: i64, cr: i64) -> i64 { return cs_clamp(y + (454*(cb-128)) / 256) } 18 19// full-frame planar RGB -> YCbCr (all three full-res planes), n = W*H pixels 20func cs_rgb_to_yuv(r: *u8, g: *u8, b: *u8, n: i64, y: *u8, cb: *u8, cr: *u8) -> i64 { 21 var i: i64 = 0 22 while i < n { 23 let rr: i64 = r[i] as i64; let gg: i64 = g[i] as i64; let bb: i64 = b[i] as i64 24 y[i] = cs_y(rr, gg, bb) as u8 25 cb[i] = cs_cb(rr, gg, bb) as u8 26 cr[i] = cs_cr(rr, gg, bb) as u8 27 i = i + 1 28 } 29 return 0 30} 31func cs_yuv_to_rgb(y: *u8, cb: *u8, cr: *u8, n: i64, r: *u8, g: *u8, b: *u8) -> i64 { 32 var i: i64 = 0 33 while i < n { 34 let yy: i64 = y[i] as i64; let u: i64 = cb[i] as i64; let v: i64 = cr[i] as i64 35 r[i] = cs_r(yy, u, v) as u8 36 g[i] = cs_g(yy, u, v) as u8 37 b[i] = cs_b(yy, u, v) as u8 38 i = i + 1 39 } 40 return 0 41} 42// 4:2:0 downsample a full-res chroma plane (W x H) -> half-res (W/2 x H/2) by 2x2 averaging. 43func cs_subsample(full: *u8, W: i64, H: i64, half: *u8) -> i64 { 44 let hw: i64 = W / 2 45 let hh: i64 = H / 2 46 var hy: i64 = 0 47 while hy < hh { 48 var hx: i64 = 0 49 while hx < hw { 50 let x: i64 = hx*2; let yy: i64 = hy*2 51 let s: i64 = (full[yy*W+x] as i64) + (full[yy*W+x+1] as i64) + (full[(yy+1)*W+x] as i64) + (full[(yy+1)*W+x+1] as i64) 52 half[hy*hw + hx] = (s / 4) as u8 53 hx = hx + 1 54 } 55 hy = hy + 1 56 } 57 return 0 58} 59// upsample a half-res chroma plane back to full (nearest -- cheap; chroma is low-freq so this is near-lossless). 60func cs_upsample(half: *u8, W: i64, H: i64, full: *u8) -> i64 { 61 let hw: i64 = W / 2 62 var y: i64 = 0 63 while y < H { 64 var x: i64 = 0 65 while x < W { full[y*W + x] = half[(y/2)*hw + (x/2)]; x = x + 1 } 66 y = y + 1 67 } 68 return 0 69}