mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
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:
parent
da38f672ae
commit
23b2c5bdf2
2 changed files with 180 additions and 0 deletions
179
node-graph/gstd/src/filter.rs
Normal file
179
node-graph/gstd/src/filter.rs
Normal 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
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue