New node: Blur (#2477)

* Implementation of gaussian blur and box blur with linear/nonlinear colorspace in raster category

* styling/formatting

* Partial code review

* remove image crate, use conversion functions from color.rs

* fix box blur checkmark, fix linear/gamma conversion

* mult/unmult alpha before/after blur

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Calvin 2025-04-29 19:47:46 -07:00 committed by GitHub
parent da38f672ae
commit 23b2c5bdf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 180 additions and 0 deletions

View file

@ -0,0 +1,179 @@
use graph_craft::proto::types::PixelLength;
use graphene_core::raster::image::{Image, ImageFrameTable};
use graphene_core::raster::{Bitmap, BitmapMut};
use graphene_core::transform::{Transform, TransformMut};
use graphene_core::{Color, Ctx};
/// Blurs the image with a Gaussian or blur kernel filter.
#[node_macro::node(category("Raster: Filter"))]
async fn blur(
_: impl Ctx,
/// The image to be blurred.
image_frame: ImageFrameTable<Color>,
/// The radius of the blur kernel.
#[range((0., 100.))]
radius: PixelLength,
/// Use a lower-quality box kernel instead of a circular Gaussian kernel. This is faster but produces boxy artifacts.
box_blur: bool,
/// Opt to incorrectly apply the filter with color calculations in gamma space for compatibility with the results from other software.
gamma: bool,
) -> ImageFrameTable<Color> {
let image_frame_transform = image_frame.transform();
let image_frame_alpha_blending = image_frame.one_instance_ref().alpha_blending;
let image = image_frame.one_instance_ref().instance.clone();
// Run blur algorithm
let blurred_image = if radius < 0.1 {
// Minimum blur radius
image.clone()
} else if box_blur {
box_blur_algorithm(image, radius, gamma)
} else {
gaussian_blur_algorithm(image, radius, gamma)
};
let mut result = ImageFrameTable::new(blurred_image);
*result.transform_mut() = image_frame_transform;
*result.one_instance_mut().alpha_blending = *image_frame_alpha_blending;
result
}
// 1D gaussian kernel
fn gaussian_kernel(radius: f64) -> Vec<f64> {
// Given radius, compute the size of the kernel that's approximately three times the radius
let kernel_radius = (3. * radius).ceil() as usize;
let kernel_size = 2 * kernel_radius + 1;
let mut gaussian_kernel: Vec<f64> = vec![0.; kernel_size];
// Kernel values
let two_radius_squared = 2. * radius * radius;
let sum = gaussian_kernel
.iter_mut()
.enumerate()
.map(|(i, value_at_index)| {
let x = i as f64 - kernel_radius as f64;
let exponent = -(x * x) / two_radius_squared;
*value_at_index = exponent.exp();
*value_at_index
})
.sum::<f64>();
// Normalize
gaussian_kernel.iter_mut().for_each(|value_at_index| *value_at_index /= sum);
gaussian_kernel
}
fn gaussian_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
if gamma {
original_buffer.map_pixels(|px| px.to_gamma_srgb().to_associated_alpha(px.a()));
} else {
original_buffer.map_pixels(|px| px.to_associated_alpha(px.a()));
}
let (width, height) = original_buffer.dimensions();
// Create 1D gaussian kernel
let kernel = gaussian_kernel(radius);
let half_kernel = kernel.len() / 2;
// Intermediate buffer for horizontal and vertical passes
let mut x_axis = Image::new(width, height, Color::TRANSPARENT);
let mut y_axis = Image::new(width, height, Color::TRANSPARENT);
for pass in [false, true] {
let (max, old_buffer, current_buffer) = match pass {
false => (width, &original_buffer, &mut x_axis),
true => (height, &x_axis, &mut y_axis),
};
let pass = pass as usize;
for y in 0..height {
for x in 0..width {
let (mut r_sum, mut g_sum, mut b_sum, mut a_sum, mut weight_sum) = (0., 0., 0., 0., 0.);
for (i, &weight) in kernel.iter().enumerate() {
let p = [x, y][pass] as i32 + (i as i32 - half_kernel as i32);
if p >= 0 && p < max as i32 {
if let Some(px) = old_buffer.get_pixel([p as u32, x][pass], [y, p as u32][pass]) {
r_sum += px.r() as f64 * weight;
g_sum += px.g() as f64 * weight;
b_sum += px.b() as f64 * weight;
a_sum += px.a() as f64 * weight;
weight_sum += weight;
}
}
}
// Normalize
let (r, g, b, a) = if weight_sum > 0. {
((r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32)
} else {
let px = old_buffer.get_pixel(x, y).unwrap();
(px.r(), px.g(), px.b(), px.a())
};
current_buffer.set_pixel(x, y, Color::from_rgbaf32_unchecked(r, g, b, a));
}
}
}
if gamma {
y_axis.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha());
} else {
y_axis.map_pixels(|px| px.to_unassociated_alpha());
}
y_axis
}
fn box_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
if gamma {
original_buffer.map_pixels(|px| px.to_gamma_srgb().to_associated_alpha(px.a()));
} else {
original_buffer.map_pixels(|px| px.to_associated_alpha(px.a()));
}
let (width, height) = original_buffer.dimensions();
let mut x_axis = Image::new(width, height, Color::TRANSPARENT);
let mut y_axis = Image::new(width, height, Color::TRANSPARENT);
for pass in [false, true] {
let (max, old_buffer, current_buffer) = match pass {
false => (width, &original_buffer, &mut x_axis),
true => (height, &x_axis, &mut y_axis),
};
let pass = pass as usize;
for y in 0..height {
for x in 0..width {
let (mut r_sum, mut g_sum, mut b_sum, mut a_sum, mut weight_sum) = (0., 0., 0., 0., 0.);
let i = [x, y][pass];
for d in (i as i32 - radius as i32).max(0)..=(i as i32 + radius as i32).min(max as i32 - 1) {
if let Some(px) = old_buffer.get_pixel([d as u32, x][pass], [y, d as u32][pass]) {
let weight = 1.;
r_sum += px.r() as f64 * weight;
g_sum += px.g() as f64 * weight;
b_sum += px.b() as f64 * weight;
a_sum += px.a() as f64 * weight;
weight_sum += weight;
}
}
let (r, g, b, a) = ((r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32);
current_buffer.set_pixel(x, y, Color::from_rgbaf32_unchecked(r, g, b, a));
}
}
}
if gamma {
y_axis.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha());
} else {
y_axis.map_pixels(|px| px.to_unassociated_alpha());
}
y_axis
}

View file

@ -8,6 +8,7 @@ pub mod vector;
pub use graphene_core::*;
pub mod brush;
pub mod dehaze;
pub mod filter;
pub mod image_color_palette;
#[cfg(feature = "wasm")]
pub mod wasm_application_io;