effectcraft/crates/raster/src/lib.rs

358 lines
12 KiB
Rust

//! Raster images for the compositor.
//!
//! [`Image`] stores premultiplied RGBA in `f32` (values may exceed 1.0 in 32 bpc projects). Every
//! heavy operation is row-parallel with rayon.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
pub mod blur;
pub mod channels3d;
pub mod cuts;
pub mod flow;
pub mod inpaint;
pub mod scopes;
pub mod warp;
use effectcraft_color::{BlendMode, blend_pixel};
use rayon::prelude::*;
pub use blur::{box_blur, directional_blur, gaussian_blur, radial_blur};
pub use channels3d::AuxChannels;
pub use warp::{Sampling, WarpOpts, accumulate_warp, composite_warp, composite_warp_reference, resample};
pub type Px = [f32; 4];
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Image {
pub width: u32,
pub height: u32,
pub data: Vec<Px>,
}
impl Image {
pub fn new(width: u32, height: u32) -> Image {
Image { width, height, data: vec![[0.0; 4]; width as usize * height as usize] }
}
pub fn filled(width: u32, height: u32, px: Px) -> Image {
Image { width, height, data: vec![px; width as usize * height as usize] }
}
pub fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0
}
#[inline]
pub fn idx(&self, x: u32, y: u32) -> usize {
y as usize * self.width as usize + x as usize
}
#[inline]
pub fn get(&self, x: i64, y: i64) -> Px {
if x < 0 || y < 0 || x >= self.width as i64 || y >= self.height as i64 { [0.0; 4] } else { self.data[y as usize * self.width as usize + x as usize] }
}
#[inline]
pub fn get_clamped(&self, x: i64, y: i64) -> Px {
let x = x.clamp(0, self.width as i64 - 1);
let y = y.clamp(0, self.height as i64 - 1);
self.data[y as usize * self.width as usize + x as usize]
}
#[inline]
pub fn set(&mut self, x: u32, y: u32, px: Px) {
let i = self.idx(x, y);
self.data[i] = px;
}
pub fn rows_mut(&mut self) -> impl IndexedParallelIterator<Item = (usize, &mut [Px])> {
let w = self.width.max(1) as usize;
self.data.par_chunks_mut(w).enumerate()
}
/// Bilinear sample at continuous pixel coordinates (pixel centres at +0.5); transparent outside.
#[inline]
pub fn sample_bilinear(&self, x: f64, y: f64) -> Px {
let fx = x - 0.5;
let fy = y - 0.5;
let x0 = fx.floor();
let y0 = fy.floor();
let tx = (fx - x0) as f32;
let ty = (fy - y0) as f32;
let (x0, y0) = (x0 as i64, y0 as i64);
let a = self.get(x0, y0);
let b = self.get(x0 + 1, y0);
let c = self.get(x0, y0 + 1);
let d = self.get(x0 + 1, y0 + 1);
let mut o = [0.0; 4];
for i in 0..4 {
let top = a[i] + (b[i] - a[i]) * tx;
let bot = c[i] + (d[i] - c[i]) * tx;
o[i] = top + (bot - top) * ty;
}
o
}
/// Bilinear with edge pixels repeated outside the image.
#[inline]
pub fn sample_bilinear_clamped(&self, x: f64, y: f64) -> Px {
let fx = x - 0.5;
let fy = y - 0.5;
let x0 = fx.floor();
let y0 = fy.floor();
let tx = (fx - x0) as f32;
let ty = (fy - y0) as f32;
let (x0, y0) = (x0 as i64, y0 as i64);
let a = self.get_clamped(x0, y0);
let b = self.get_clamped(x0 + 1, y0);
let c = self.get_clamped(x0, y0 + 1);
let d = self.get_clamped(x0 + 1, y0 + 1);
let mut o = [0.0; 4];
for i in 0..4 {
let top = a[i] + (b[i] - a[i]) * tx;
let bot = c[i] + (d[i] - c[i]) * tx;
o[i] = top + (bot - top) * ty;
}
o
}
/// Catmull-Rom bicubic sample; transparent outside.
pub fn sample_bicubic(&self, x: f64, y: f64) -> Px {
let fx = x - 0.5;
let fy = y - 0.5;
let x0 = fx.floor();
let y0 = fy.floor();
let tx = fx - x0;
let ty = fy - y0;
let w = |t: f64| -> [f32; 4] {
let t2 = t * t;
let t3 = t2 * t;
[(-0.5 * t3 + t2 - 0.5 * t) as f32, (1.5 * t3 - 2.5 * t2 + 1.0) as f32, (-1.5 * t3 + 2.0 * t2 + 0.5 * t) as f32, (0.5 * t3 - 0.5 * t2) as f32]
};
let wx = w(tx);
let wy = w(ty);
let mut o = [0.0f32; 4];
for j in 0..4 {
for i in 0..4 {
let p = self.get(x0 as i64 - 1 + i as i64, y0 as i64 - 1 + j as i64);
let k = wx[i] * wy[j];
for c in 0..4 {
o[c] += p[c] * k;
}
}
}
// Keep premultiplied invariants after ringing.
o[3] = o[3].max(0.0);
for c in 0..3 {
o[c] = o[c].max(0.0);
}
o
}
/// Copy with `pad` transparent pixels on every side.
pub fn padded(&self, pad: u32) -> Image {
let mut out = Image::new(self.width + 2 * pad, self.height + 2 * pad);
let ow = out.width as usize;
for y in 0..self.height as usize {
let src = &self.data[y * self.width as usize..(y + 1) * self.width as usize];
let o = (y + pad as usize) * ow + pad as usize;
out.data[o..o + self.width as usize].copy_from_slice(src);
}
out
}
/// Sub-rectangle copy (clipped).
pub fn crop(&self, x: i64, y: i64, w: u32, h: u32) -> Image {
let mut out = Image::new(w, h);
out.rows_mut().for_each(|(oy, row)| {
for (ox, px) in row.iter_mut().enumerate() {
*px = self.get(x + ox as i64, y + oy as i64);
}
});
out
}
/// Multiply every pixel (premultiplied) by `k`.
pub fn scale_alpha(&mut self, k: f32) {
if (k - 1.0).abs() < 1e-7 {
return;
}
self.data.par_iter_mut().for_each(|p| {
for c in p.iter_mut() {
*c *= k;
}
});
}
/// Multiply each pixel by a coverage value from `mask` (same size).
pub fn mul_mask(&mut self, mask: &[f32]) {
self.data.par_iter_mut().zip(mask.par_iter()).for_each(|(p, &m)| {
for c in p.iter_mut() {
*c *= m;
}
});
}
/// Clamp to [0, 1] (8/16 bpc working space).
pub fn clamp01(&mut self) {
self.data.par_iter_mut().for_each(|p| {
for c in p.iter_mut() {
*c = c.clamp(0.0, 1.0);
}
for c in 0..3 {
p[c] = p[c].min(p[3]);
}
});
}
/// Quantise to `levels` per channel (simulates 8/16 bpc output).
pub fn quantize(&mut self, levels: f32) {
self.data.par_iter_mut().for_each(|p| {
for c in p.iter_mut() {
*c = (*c * levels).round() / levels;
}
});
}
/// Straight-alpha 8-bit RGBA (sRGB-encoded values as stored).
pub fn to_rgba8(&self) -> Vec<u8> {
let mut out = vec![0u8; self.data.len() * 4];
out.par_chunks_mut(4).zip(self.data.par_iter()).for_each(|(o, p)| {
let a = p[3].clamp(0.0, 1.0);
let inv = if a > 0.0 { 1.0 / p[3] } else { 0.0 };
for c in 0..3 {
o[c] = ((p[c] * inv).clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
}
o[3] = (a * 255.0 + 0.5) as u8;
});
out
}
/// Composite over an opaque background colour and return opaque 8-bit RGBA.
pub fn to_rgba8_over(&self, bg: [f32; 3]) -> Vec<u8> {
let mut out = vec![0u8; self.data.len() * 4];
out.par_chunks_mut(4).zip(self.data.par_iter()).for_each(|(o, p)| {
let k = 1.0 - p[3].clamp(0.0, 1.0);
for c in 0..3 {
o[c] = ((p[c] + bg[c] * k).clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
}
o[3] = 255;
});
out
}
/// From straight-alpha 8-bit RGBA.
pub fn from_rgba8(width: u32, height: u32, rgba: &[u8]) -> Image {
let mut img = Image::new(width, height);
img.data.par_iter_mut().zip(rgba.par_chunks(4)).for_each(|(p, s)| {
let a = s[3] as f32 / 255.0;
*p = [s[0] as f32 / 255.0 * a, s[1] as f32 / 255.0 * a, s[2] as f32 / 255.0 * a, a];
});
img
}
/// Straight-alpha colour at a pixel.
pub fn straight(&self, x: i64, y: i64) -> Px {
let p = self.get(x, y);
if p[3] > 0.0 { [p[0] / p[3], p[1] / p[3], p[2] / p[3], p[3]] } else { [0.0; 4] }
}
/// Map every pixel through `f` on straight colour (keeps alpha).
pub fn map_straight(&mut self, f: impl Fn([f32; 3]) -> [f32; 3] + Sync) {
self.data.par_iter_mut().for_each(|p| {
let a = p[3];
if a <= 0.0 {
return;
}
let c = f([p[0] / a, p[1] / a, p[2] / a]);
*p = [c[0] * a, c[1] * a, c[2] * a, a];
});
}
/// Blend `src` (same size) onto self with a mode and opacity.
pub fn blend_from(&mut self, src: &Image, mode: BlendMode, opacity: f32, seed: u32) {
let w = self.width as usize;
self.data.par_chunks_mut(w.max(1)).zip(src.data.par_chunks(w.max(1))).enumerate().for_each(|(y, (d, s))| {
for x in 0..d.len() {
let mut sp = s[x];
if sp[3] <= 0.0 && !mode.is_stencil() {
continue;
}
for c in sp.iter_mut() {
*c *= opacity;
}
let n = if matches!(mode, BlendMode::Dissolve | BlendMode::DancingDissolve) { hash_noise(x as u32, y as u32, seed) } else { 0.5 };
d[x] = blend_pixel(mode, d[x], sp, n);
}
});
}
/// Bounding box of non-transparent pixels (x0, y0, x1, y1), None if empty.
pub fn alpha_bounds(&self) -> Option<(u32, u32, u32, u32)> {
let w = self.width as usize;
let mut b: Option<(u32, u32, u32, u32)> = None;
for (y, row) in self.data.chunks(w.max(1)).enumerate() {
if let Some(first) = row.iter().position(|p| p[3] > 1e-6) {
let last = row.iter().rposition(|p| p[3] > 1e-6).unwrap_or(first);
let (y, f, l) = (y as u32, first as u32, last as u32 + 1);
b = Some(match b {
None => (f, y, l, y + 1),
Some((x0, y0, x1, _)) => (x0.min(f), y0, x1.max(l), y + 1),
});
}
}
b
}
}
/// Deterministic per-pixel noise in 0..1.
#[inline]
pub fn hash_noise(x: u32, y: u32, seed: u32) -> f32 {
let mut h = x.wrapping_mul(0x8da6_b343) ^ y.wrapping_mul(0xd816_3841) ^ seed.wrapping_mul(0xcb1a_b31f);
h ^= h >> 13;
h = h.wrapping_mul(0x5bd1_e995);
h ^= h >> 15;
(h & 0x00ff_ffff) as f32 / 16_777_216.0
}
/// A single-channel coverage mask.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Mask {
pub width: u32,
pub height: u32,
pub data: Vec<f32>,
}
impl Mask {
pub fn new(width: u32, height: u32, v: f32) -> Mask {
Mask { width, height, data: vec![v; width as usize * height as usize] }
}
pub fn to_image(&self) -> Image {
Image { width: self.width, height: self.height, data: self.data.iter().map(|&a| [a, a, a, a]).collect() }
}
pub fn from_alpha(img: &Image) -> Mask {
Mask { width: img.width, height: img.height, data: img.data.iter().map(|p| p[3]).collect() }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgba8_roundtrip() {
let src: Vec<u8> = vec![255, 128, 0, 255, 10, 20, 30, 128, 0, 0, 0, 0, 1, 2, 3, 4];
let img = Image::from_rgba8(2, 2, &src);
let back = img.to_rgba8();
for (a, b) in src.chunks(4).zip(back.chunks(4)) {
if a[3] >= 128 {
assert!(a.iter().zip(b).all(|(x, y)| (*x as i32 - *y as i32).abs() <= 1), "{a:?} {b:?}");
} else {
assert!((a[3] as i32 - b[3] as i32).abs() <= 1);
}
}
}
#[test]
fn bilinear_at_centres_is_exact() {
let mut img = Image::new(3, 3);
img.set(1, 1, [0.5, 0.25, 0.125, 1.0]);
assert_eq!(img.sample_bilinear(1.5, 1.5), [0.5, 0.25, 0.125, 1.0]);
let half = img.sample_bilinear(2.0, 1.5);
assert!((half[3] - 0.5).abs() < 1e-6);
}
#[test]
fn alpha_bounds_found() {
let mut img = Image::new(10, 10);
img.set(3, 4, [1.0; 4]);
img.set(6, 7, [1.0; 4]);
assert_eq!(img.alpha_bounds(), Some((3, 4, 7, 8)));
}
#[test]
fn noise_in_range() {
for i in 0..1000 {
let n = hash_noise(i, i * 7, 3);
assert!((0.0..1.0).contains(&n));
}
}
}