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}