Something went wrong. Try again.
iterative image reconstruction using random cubic bézier strokes, accelerated on metal
metal graphics rust
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869use rayon::prelude::*;
#[derive(Clone, Copy, Debug, Default)]pub struct Oklab { pub l: f32, pub a: f32, pub b: f32, }#[derive(Clone, Copy, Debug, Default)]pub struct Srgb { pub r: f32, pub g: f32, pub b: f32, }
impl Srgb { pub fn from_u8(r: u8, g: u8, b: u8) -> Self { Self { r: r as f32 / 255.0, g: g as f32 / 255.0, b: b as f32 / 255.0, } } pub fn to_u8(self) -> (u8, u8, u8) { let clamp = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8; (clamp(self.r), clamp(self.g), clamp(self.b)) }
fn linearize(c: f32) -> f32 { if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) } } fn delinearize(c: f32) -> f32 { if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 } }
pub fn to_linear(self) -> (f32, f32, f32) { ( Self::linearize(self.r), Self::linearize(self.g), Self::linearize(self.b), ) } pub fn from_linear(r: f32, g: f32, b: f32) -> Self { Self { r: Self::delinearize(r), g: Self::delinearize(g), b: Self::delinearize(b), } } }
impl Oklab { pub fn from_srgb(srgb: Srgb) -> Self { // srgb to linear srgb let (r, g, b) = srgb.to_linear();
// linear srgb to lms (approximate cone response) let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b; let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
// apply non-linearity (cube root) let l_ = l.cbrt(); let m_ = m.cbrt(); let s_ = s.cbrt();
// transform to oklab coordinates (matrix m2) Self { l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_ , a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_ , b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_ } }
pub fn to_srgb(self) -> Srgb { // inverse of matrix m2 (oklab -> non-linear lms) let l_ = self.l + 0.3963377774 * self.a + 0.2158037573 * self.b; let m_ = self.l - 0.1055613458 * self.a - 0.0638541728 * self.b; let s_ = self.l - 0.0894841775 * self.a - 1.2914855480 * self.b;
// revert non-linearity (cube) let l = l_ * l_ * l_; let m = m_ * m_ * m_; let s = s_ * s_ * s_;
// inverse of matrix m1 (lms -> linear srgb) let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
// convert linear srgb to srgb Srgb::from_linear(r, g, b) } }
pub fn srgb8_to_oklab(r: u8, g: u8, b: u8) -> Oklab { Oklab::from_srgb(Srgb::from_u8(r, g, b)) }pub fn oklab_to_srgb8(lab: Oklab) -> (u8, u8, u8) { lab.to_srgb().to_u8() }
pub fn rgb8_to_oklab_parallel(rgb: &[u8]) -> Result<Vec<Oklab>, String> { if rgb.len() % 3 != 0 { return Err("rgb buffer must be 3 bytes per pixel".into()); } Ok(rgb.par_chunks(3).map(|c| srgb8_to_oklab(c[0], c[1], c[2])).collect()) }
pub fn avg_oklab_parallel(pixels: &[Oklab]) -> Oklab { if pixels.is_empty() { return Oklab::default(); } let (l, a, b) = pixels.par_iter().map(|p| (p.l, p.a, p.b)).reduce(|| (0.0, 0.0, 0.0), |(l1, a1, b1), (l2, a2, b2)| (l1 + l2, a1 + a2, b1 + b2)); let n = pixels.len() as f32; Oklab { l: l / n, a: a / n, b: b / n } }