From 4182032236f7139c1d613157ef9ea9b8d39faa94 Mon Sep 17 00:00:00 2001 From: Calvin Vu Date: Mon, 24 Mar 2025 00:50:41 -0700 Subject: [PATCH 1/7] Implementation of gaussian blur and box blur with linear/nonlinear colorspace in raster category --- node-graph/gstd/src/filter.rs | 262 ++++++++++++++++++++++++++++++++++ node-graph/gstd/src/lib.rs | 2 + 2 files changed, 264 insertions(+) create mode 100644 node-graph/gstd/src/filter.rs diff --git a/node-graph/gstd/src/filter.rs b/node-graph/gstd/src/filter.rs new file mode 100644 index 0000000000..0a494673d6 --- /dev/null +++ b/node-graph/gstd/src/filter.rs @@ -0,0 +1,262 @@ +use graph_craft::proto::types::PixelLength; +use graphene_core::raster::Channel; +use graphene_core::raster::image::{Image, ImageFrameTable}; +use graphene_core::transform::{Transform, TransformMut}; +use graphene_core::{Color, Ctx}; +use image::{DynamicImage, ImageBuffer, Rgba}; + +#[node_macro::node(category("Raster"))] +async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100.))] radius: PixelLength, gaussian_blur: bool, nonlinear: bool) -> ImageFrameTable { + let image_frame_transform = image_frame.transform(); + let image_frame_alpha_blending = image_frame.one_instance().alpha_blending; + + let image = image_frame.one_instance().instance; + + // Prepare the image data for processing + let image_data = bytemuck::cast_vec(image.data.clone()); + let mut image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type."); + + // Run blur algorithm + let blurred_image = blur_helper(&mut image_buffer, radius, gaussian_blur, nonlinear); + + // Prepare the image data for returning + let buffer = blurred_image.to_rgba32f().into_raw(); + let color_vec = bytemuck::cast_vec(buffer); + let processed_image = Image { + width: image.width, + height: image.height, + data: color_vec, + base64_string: None, + }; + + let mut result = ImageFrameTable::new(processed_image); + *result.transform_mut() = image_frame_transform; + *result.one_instance_mut().alpha_blending = *image_frame_alpha_blending; + + result +} + +// Helpers to convert image buffer to linear/nonlinear color spaces in-place +fn to_linear_helper(image_buffer: &mut ImageBuffer, Vec>) { + for pixel in image_buffer.pixels_mut() { + // Leave alpha channels + let channels = pixel.0; + pixel.0[0] = channels[0].to_linear(); + pixel.0[1] = channels[1].to_linear(); + pixel.0[2] = channels[2].to_linear(); + } +} +fn from_linear_helper(image_buffer: &mut ImageBuffer, Vec>) { + for pixel in image_buffer.pixels_mut() { + let channels = pixel.0; + pixel.0[0] = Channel::from_linear(channels[0]); + pixel.0[1] = Channel::from_linear(channels[1]); + pixel.0[2] = Channel::from_linear(channels[2]); + } +} + +fn blur_helper(image_buffer: &mut ImageBuffer, Vec>, radius: f64, gaussian: bool, nonlinear: bool) -> DynamicImage { + // For small radius, image would not change much -> just return original image + if radius < 1 as f64 { + return image_buffer.clone().into(); + } else { + // Convert to linear color space by default + if !nonlinear { + to_linear_helper(image_buffer); + } + // Run the gaussian blur algorithm, if user wants + if gaussian { + return gaussian_blur(image_buffer.clone(), radius, nonlinear); + } + // Else, run box blur + else { + return box_blur(image_buffer.clone(), radius, nonlinear); + } + } +} + +fn gaussian_blur(original_buffer: ImageBuffer, Vec>, radius: f64, nonlinear: bool) -> DynamicImage { + let (width, height) = original_buffer.dimensions(); + + // Create 1D gaussian kernel + let kernel = create_gaussian_kernel(radius); + let half_kernel = kernel.len() / 2; + + // Intermediate buffer for horizontal pass + let mut x_axis = ImageBuffer::, Vec>::new(width, height); + // Blur along x-axis + for y in 0..height { + for x in 0..width { + let mut r_sum = 0.0; + let mut g_sum = 0.0; + let mut b_sum = 0.0; + let mut a_sum = 0.0; + let mut weight_sum = 0.0; + + for (i, &weight) in kernel.iter().enumerate() { + let kx = i as i32 - half_kernel as i32; + let px = x as i32 + kx; + + if px >= 0 && px < width as i32 { + let pixel = original_buffer.get_pixel(px as u32, y); + + r_sum += pixel[0] as f64 * weight; + g_sum += pixel[1] as f64 * weight; + b_sum += pixel[2] as f64 * weight; + a_sum += pixel[3] as f64 * weight; + weight_sum += weight; + } + } + + // Normalize + if weight_sum > 0.0 { + let r = (r_sum / weight_sum) as f32; + let g = (g_sum / weight_sum) as f32; + let b = (b_sum / weight_sum) as f32; + let a = (a_sum / weight_sum) as f32; + + x_axis.put_pixel(x, y, Rgba([r, g, b, a])); + } else { + x_axis.put_pixel(x, y, *original_buffer.get_pixel(x, y)); + } + } + } + + // Intermediate buffer for vertical pass + let mut y_axis = ImageBuffer::, Vec>::new(width, height); + // Blur along y-axis + for y in 0..height { + for x in 0..width { + let mut r_sum = 0.0; + let mut g_sum = 0.0; + let mut b_sum = 0.0; + let mut a_sum: f64 = 0.0; + let mut weight_sum = 0.0; + + for (i, &weight) in kernel.iter().enumerate() { + let ky = i as i32 - half_kernel as i32; + let py = y as i32 + ky; + + if py >= 0 && py < height as i32 { + let pixel = x_axis.get_pixel(x, py as u32); + + r_sum += pixel[0] as f64 * weight; + g_sum += pixel[1] as f64 * weight; + b_sum += pixel[2] as f64 * weight; + a_sum += pixel[3] as f64 * weight; + weight_sum += weight; + } + } + + if weight_sum > 0.0 { + let r = (r_sum / weight_sum) as f32; + let g = (g_sum / weight_sum) as f32; + let b = (b_sum / weight_sum) as f32; + let a = (a_sum / weight_sum) as f32; + + y_axis.put_pixel(x, y, Rgba([r, g, b, a])); + } else { + y_axis.put_pixel(x, y, *x_axis.get_pixel(x, y)); + } + } + } + + // Convert linear back to nonlinear if converted initially + if !nonlinear { + from_linear_helper(&mut y_axis); + } + DynamicImage::ImageRgba32F(y_axis) +} + +// 1D gaussian kernel +fn create_gaussian_kernel(radius: f64) -> Vec { + // Given radius, compute size of kernel -> 3*radius (approx.) + let kernel_radius = (3.0 * radius).ceil() as usize; + let kernel_size = 2 * kernel_radius + 1; + let mut gaussian_kernel: Vec = vec![0.0; kernel_size]; + + // Kernel values + let two_radius_squared = 2.0 * radius * radius; + let mut sum = 0.0; + for i in 0..kernel_size { + let x: f64 = i as f64 - kernel_radius as f64; + let exponent = -(x * x) / two_radius_squared; + gaussian_kernel[i] = exponent.exp(); + sum += gaussian_kernel[i]; + } + + // Normalize + for i in 0..kernel_size { + gaussian_kernel[i] /= sum; + } + + gaussian_kernel +} + +fn box_blur(original_buffer: ImageBuffer, Vec>, radius: f64, nonlinear: bool) -> DynamicImage { + let (width, height) = original_buffer.dimensions(); + let mut x_axis = ImageBuffer::new(width, height); + let mut blurred_image = ImageBuffer::new(width, height); + + // Blur along x-axis + for y in 0..height { + for x in 0..width { + let mut r_sum = 0.0; + let mut g_sum = 0.0; + let mut b_sum = 0.0; + let mut a_sum = 0.0; + let mut weight_sum = 0.0; + + for dx in (x as i32 - radius as i32).max(0)..=(x as i32 + radius as i32).min(width as i32 - 1) { + let pixel = original_buffer.get_pixel(dx as u32, y); + let weight = 1.0; + + r_sum += pixel[0] as f64 * weight; + g_sum += pixel[1] as f64 * weight; + b_sum += pixel[2] as f64 * weight; + a_sum += pixel[3] as f64 * weight; + weight_sum += weight; + } + + x_axis.put_pixel( + x, + y, + Rgba([(r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32]), + ); + } + } + + // Blur along y-axis + for y in 0..height { + for x in 0..width { + let mut r_sum = 0.0; + let mut g_sum = 0.0; + let mut b_sum = 0.0; + let mut a_sum = 0.0; + let mut weight_sum = 0.0; + + for dy in (y as i32 - radius as i32).max(0)..=(y as i32 + radius as i32).min(height as i32 - 1) { + let pixel = x_axis.get_pixel(x, dy as u32); + let weight = 1.0; + + r_sum += pixel[0] as f64 * weight; + g_sum += pixel[1] as f64 * weight; + b_sum += pixel[2] as f64 * weight; + a_sum += pixel[3] as f64 * weight; + weight_sum += weight; + } + + blurred_image.put_pixel( + x, + y, + Rgba([(r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32]), + ); + } + } + + // Convert linear back to nonlinear if converted initially + if !nonlinear { + from_linear_helper(&mut blurred_image); + } + DynamicImage::ImageRgba32F(blurred_image) +} diff --git a/node-graph/gstd/src/lib.rs b/node-graph/gstd/src/lib.rs index 4ec7e1151f..b7ba14423a 100644 --- a/node-graph/gstd/src/lib.rs +++ b/node-graph/gstd/src/lib.rs @@ -30,3 +30,5 @@ pub mod wasm_application_io; pub mod dehaze; pub mod imaginate; + +pub mod filter; From a8de7ed6c1ba85c85042fca74ec5569c8f723112 Mon Sep 17 00:00:00 2001 From: Calvin Vu Date: Tue, 25 Mar 2025 22:59:41 -0700 Subject: [PATCH 2/7] styling/formatting --- node-graph/gstd/src/filter.rs | 154 +++++++++++++++++----------------- 1 file changed, 79 insertions(+), 75 deletions(-) diff --git a/node-graph/gstd/src/filter.rs b/node-graph/gstd/src/filter.rs index 0a494673d6..19480f1ff6 100644 --- a/node-graph/gstd/src/filter.rs +++ b/node-graph/gstd/src/filter.rs @@ -5,6 +5,11 @@ use graphene_core::transform::{Transform, TransformMut}; use graphene_core::{Color, Ctx}; use image::{DynamicImage, ImageBuffer, Rgba}; +enum ConvertFunction { + ToLinear, + ToNonlinear, +} + #[node_macro::node(category("Raster"))] async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100.))] radius: PixelLength, gaussian_blur: bool, nonlinear: bool) -> ImageFrameTable { let image_frame_transform = image_frame.transform(); @@ -36,41 +41,44 @@ async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100 result } -// Helpers to convert image buffer to linear/nonlinear color spaces in-place -fn to_linear_helper(image_buffer: &mut ImageBuffer, Vec>) { +// Helper to convert image buffer to linear/nonlinear color spaces in-place +fn convert_color_space(image_buffer: &mut ImageBuffer, Vec>, convert: ConvertFunction) { for pixel in image_buffer.pixels_mut() { // Leave alpha channels let channels = pixel.0; - pixel.0[0] = channels[0].to_linear(); - pixel.0[1] = channels[1].to_linear(); - pixel.0[2] = channels[2].to_linear(); - } -} -fn from_linear_helper(image_buffer: &mut ImageBuffer, Vec>) { - for pixel in image_buffer.pixels_mut() { - let channels = pixel.0; - pixel.0[0] = Channel::from_linear(channels[0]); - pixel.0[1] = Channel::from_linear(channels[1]); - pixel.0[2] = Channel::from_linear(channels[2]); + + match convert { + ConvertFunction::ToLinear => { + pixel.0[0] = channels[0].to_linear(); + pixel.0[1] = channels[1].to_linear(); + pixel.0[2] = channels[2].to_linear(); + } + + ConvertFunction::ToNonlinear => { + pixel.0[0] = Channel::from_linear(channels[0]); + pixel.0[1] = Channel::from_linear(channels[1]); + pixel.0[2] = Channel::from_linear(channels[2]); + } + } } } fn blur_helper(image_buffer: &mut ImageBuffer, Vec>, radius: f64, gaussian: bool, nonlinear: bool) -> DynamicImage { // For small radius, image would not change much -> just return original image - if radius < 1 as f64 { - return image_buffer.clone().into(); + if radius < 1_f64 { + image_buffer.clone().into() } else { // Convert to linear color space by default if !nonlinear { - to_linear_helper(image_buffer); + convert_color_space(image_buffer, ConvertFunction::ToLinear) } // Run the gaussian blur algorithm, if user wants if gaussian { - return gaussian_blur(image_buffer.clone(), radius, nonlinear); + gaussian_blur(image_buffer.clone(), radius, nonlinear) } // Else, run box blur else { - return box_blur(image_buffer.clone(), radius, nonlinear); + box_blur(image_buffer.clone(), radius, nonlinear) } } } @@ -87,11 +95,11 @@ fn gaussian_blur(original_buffer: ImageBuffer, Vec>, radius: f64, // Blur along x-axis for y in 0..height { for x in 0..width { - let mut r_sum = 0.0; - let mut g_sum = 0.0; - let mut b_sum = 0.0; - let mut a_sum = 0.0; - let mut weight_sum = 0.0; + let mut r_sum = 0.; + let mut g_sum = 0.; + let mut b_sum = 0.; + let mut a_sum = 0.; + let mut weight_sum = 0.; for (i, &weight) in kernel.iter().enumerate() { let kx = i as i32 - half_kernel as i32; @@ -109,16 +117,7 @@ fn gaussian_blur(original_buffer: ImageBuffer, Vec>, radius: f64, } // Normalize - if weight_sum > 0.0 { - let r = (r_sum / weight_sum) as f32; - let g = (g_sum / weight_sum) as f32; - let b = (b_sum / weight_sum) as f32; - let a = (a_sum / weight_sum) as f32; - - x_axis.put_pixel(x, y, Rgba([r, g, b, a])); - } else { - x_axis.put_pixel(x, y, *original_buffer.get_pixel(x, y)); - } + normalize(&mut x_axis, &original_buffer, weight_sum, (r_sum, b_sum, g_sum, a_sum), x, y); } } @@ -127,11 +126,11 @@ fn gaussian_blur(original_buffer: ImageBuffer, Vec>, radius: f64, // Blur along y-axis for y in 0..height { for x in 0..width { - let mut r_sum = 0.0; - let mut g_sum = 0.0; - let mut b_sum = 0.0; - let mut a_sum: f64 = 0.0; - let mut weight_sum = 0.0; + let mut r_sum = 0.; + let mut g_sum = 0.; + let mut b_sum = 0.; + let mut a_sum: f64 = 0.; + let mut weight_sum = 0.; for (i, &weight) in kernel.iter().enumerate() { let ky = i as i32 - half_kernel as i32; @@ -148,47 +147,52 @@ fn gaussian_blur(original_buffer: ImageBuffer, Vec>, radius: f64, } } - if weight_sum > 0.0 { - let r = (r_sum / weight_sum) as f32; - let g = (g_sum / weight_sum) as f32; - let b = (b_sum / weight_sum) as f32; - let a = (a_sum / weight_sum) as f32; - - y_axis.put_pixel(x, y, Rgba([r, g, b, a])); - } else { - y_axis.put_pixel(x, y, *x_axis.get_pixel(x, y)); - } + normalize(&mut y_axis, &x_axis, weight_sum, (r_sum, b_sum, g_sum, a_sum), x, y); } } // Convert linear back to nonlinear if converted initially if !nonlinear { - from_linear_helper(&mut y_axis); + convert_color_space(&mut y_axis, ConvertFunction::ToNonlinear); } DynamicImage::ImageRgba32F(y_axis) } +fn normalize(current_buffer: &mut ImageBuffer, Vec>, old_buffer: &ImageBuffer, Vec>, weight_sum: f64, rgba: (f64, f64, f64, f64), x: u32, y: u32) { + if weight_sum > 0. { + let r = (rgba.0 / weight_sum) as f32; + let g = (rgba.1 / weight_sum) as f32; + let b = (rgba.2 / weight_sum) as f32; + let a = (rgba.3 / weight_sum) as f32; + + current_buffer.put_pixel(x, y, Rgba([r, g, b, a])); + } else { + current_buffer.put_pixel(x, y, *old_buffer.get_pixel(x, y)); + } +} + // 1D gaussian kernel fn create_gaussian_kernel(radius: f64) -> Vec { // Given radius, compute size of kernel -> 3*radius (approx.) - let kernel_radius = (3.0 * radius).ceil() as usize; + let kernel_radius = (3. * radius).ceil() as usize; let kernel_size = 2 * kernel_radius + 1; - let mut gaussian_kernel: Vec = vec![0.0; kernel_size]; + let mut gaussian_kernel: Vec = vec![0.; kernel_size]; // Kernel values - let two_radius_squared = 2.0 * radius * radius; - let mut sum = 0.0; - for i in 0..kernel_size { - let x: f64 = i as f64 - kernel_radius as f64; - let exponent = -(x * x) / two_radius_squared; - gaussian_kernel[i] = exponent.exp(); - sum += gaussian_kernel[i]; - } + let two_radius_squared = 2. * radius * radius; + let sum: f64 = 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(); // Normalize - for i in 0..kernel_size { - gaussian_kernel[i] /= sum; - } + gaussian_kernel.iter_mut().for_each(|value_at_index| *value_at_index /= sum); gaussian_kernel } @@ -201,15 +205,15 @@ fn box_blur(original_buffer: ImageBuffer, Vec>, radius: f64, nonl // Blur along x-axis for y in 0..height { for x in 0..width { - let mut r_sum = 0.0; - let mut g_sum = 0.0; - let mut b_sum = 0.0; - let mut a_sum = 0.0; - let mut weight_sum = 0.0; + let mut r_sum = 0.; + let mut g_sum = 0.; + let mut b_sum = 0.; + let mut a_sum = 0.; + let mut weight_sum = 0.; for dx in (x as i32 - radius as i32).max(0)..=(x as i32 + radius as i32).min(width as i32 - 1) { let pixel = original_buffer.get_pixel(dx as u32, y); - let weight = 1.0; + let weight = 1.; r_sum += pixel[0] as f64 * weight; g_sum += pixel[1] as f64 * weight; @@ -229,15 +233,15 @@ fn box_blur(original_buffer: ImageBuffer, Vec>, radius: f64, nonl // Blur along y-axis for y in 0..height { for x in 0..width { - let mut r_sum = 0.0; - let mut g_sum = 0.0; - let mut b_sum = 0.0; - let mut a_sum = 0.0; - let mut weight_sum = 0.0; + let mut r_sum = 0.; + let mut g_sum = 0.; + let mut b_sum = 0.; + let mut a_sum = 0.; + let mut weight_sum = 0.; for dy in (y as i32 - radius as i32).max(0)..=(y as i32 + radius as i32).min(height as i32 - 1) { let pixel = x_axis.get_pixel(x, dy as u32); - let weight = 1.0; + let weight = 1.; r_sum += pixel[0] as f64 * weight; g_sum += pixel[1] as f64 * weight; @@ -256,7 +260,7 @@ fn box_blur(original_buffer: ImageBuffer, Vec>, radius: f64, nonl // Convert linear back to nonlinear if converted initially if !nonlinear { - from_linear_helper(&mut blurred_image); + convert_color_space(&mut blurred_image, ConvertFunction::ToNonlinear); } DynamicImage::ImageRgba32F(blurred_image) } From 1f5ebc9a01370abe5b09f06a4120704206affab7 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 14 Apr 2025 03:51:11 -0700 Subject: [PATCH 3/7] Partial code review --- node-graph/gstd/src/filter.rs | 284 +++++++++++++--------------------- 1 file changed, 111 insertions(+), 173 deletions(-) diff --git a/node-graph/gstd/src/filter.rs b/node-graph/gstd/src/filter.rs index 19480f1ff6..39d7af447e 100644 --- a/node-graph/gstd/src/filter.rs +++ b/node-graph/gstd/src/filter.rs @@ -7,11 +7,11 @@ use image::{DynamicImage, ImageBuffer, Rgba}; enum ConvertFunction { ToLinear, - ToNonlinear, + ToGamma, } #[node_macro::node(category("Raster"))] -async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100.))] radius: PixelLength, gaussian_blur: bool, nonlinear: bool) -> ImageFrameTable { +async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100.))] radius: PixelLength, box_blur: bool, gamma: bool) -> ImageFrameTable { let image_frame_transform = image_frame.transform(); let image_frame_alpha_blending = image_frame.one_instance().alpha_blending; @@ -19,10 +19,17 @@ async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100 // Prepare the image data for processing let image_data = bytemuck::cast_vec(image.data.clone()); - let mut image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type."); + let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type."); // Run blur algorithm - let blurred_image = blur_helper(&mut image_buffer, radius, gaussian_blur, nonlinear); + let blurred_image = if radius < 0.1 { + // Minimum blur radius + image_buffer.into() + } else if box_blur { + gaussian_blur_algorithm(image_buffer, radius, gamma) + } else { + box_blur_algorithm(image_buffer, radius, gamma) + }; // Prepare the image data for returning let buffer = blurred_image.to_rgba32f().into_raw(); @@ -53,8 +60,7 @@ fn convert_color_space(image_buffer: &mut ImageBuffer, Vec>, conv pixel.0[1] = channels[1].to_linear(); pixel.0[2] = channels[2].to_linear(); } - - ConvertFunction::ToNonlinear => { + ConvertFunction::ToGamma => { pixel.0[0] = Channel::from_linear(channels[0]); pixel.0[1] = Channel::from_linear(channels[1]); pixel.0[2] = Channel::from_linear(channels[2]); @@ -63,116 +69,8 @@ fn convert_color_space(image_buffer: &mut ImageBuffer, Vec>, conv } } -fn blur_helper(image_buffer: &mut ImageBuffer, Vec>, radius: f64, gaussian: bool, nonlinear: bool) -> DynamicImage { - // For small radius, image would not change much -> just return original image - if radius < 1_f64 { - image_buffer.clone().into() - } else { - // Convert to linear color space by default - if !nonlinear { - convert_color_space(image_buffer, ConvertFunction::ToLinear) - } - // Run the gaussian blur algorithm, if user wants - if gaussian { - gaussian_blur(image_buffer.clone(), radius, nonlinear) - } - // Else, run box blur - else { - box_blur(image_buffer.clone(), radius, nonlinear) - } - } -} - -fn gaussian_blur(original_buffer: ImageBuffer, Vec>, radius: f64, nonlinear: bool) -> DynamicImage { - let (width, height) = original_buffer.dimensions(); - - // Create 1D gaussian kernel - let kernel = create_gaussian_kernel(radius); - let half_kernel = kernel.len() / 2; - - // Intermediate buffer for horizontal pass - let mut x_axis = ImageBuffer::, Vec>::new(width, height); - // Blur along x-axis - for y in 0..height { - for x in 0..width { - let mut r_sum = 0.; - let mut g_sum = 0.; - let mut b_sum = 0.; - let mut a_sum = 0.; - let mut weight_sum = 0.; - - for (i, &weight) in kernel.iter().enumerate() { - let kx = i as i32 - half_kernel as i32; - let px = x as i32 + kx; - - if px >= 0 && px < width as i32 { - let pixel = original_buffer.get_pixel(px as u32, y); - - r_sum += pixel[0] as f64 * weight; - g_sum += pixel[1] as f64 * weight; - b_sum += pixel[2] as f64 * weight; - a_sum += pixel[3] as f64 * weight; - weight_sum += weight; - } - } - - // Normalize - normalize(&mut x_axis, &original_buffer, weight_sum, (r_sum, b_sum, g_sum, a_sum), x, y); - } - } - - // Intermediate buffer for vertical pass - let mut y_axis = ImageBuffer::, Vec>::new(width, height); - // Blur along y-axis - for y in 0..height { - for x in 0..width { - let mut r_sum = 0.; - let mut g_sum = 0.; - let mut b_sum = 0.; - let mut a_sum: f64 = 0.; - let mut weight_sum = 0.; - - for (i, &weight) in kernel.iter().enumerate() { - let ky = i as i32 - half_kernel as i32; - let py = y as i32 + ky; - - if py >= 0 && py < height as i32 { - let pixel = x_axis.get_pixel(x, py as u32); - - r_sum += pixel[0] as f64 * weight; - g_sum += pixel[1] as f64 * weight; - b_sum += pixel[2] as f64 * weight; - a_sum += pixel[3] as f64 * weight; - weight_sum += weight; - } - } - - normalize(&mut y_axis, &x_axis, weight_sum, (r_sum, b_sum, g_sum, a_sum), x, y); - } - } - - // Convert linear back to nonlinear if converted initially - if !nonlinear { - convert_color_space(&mut y_axis, ConvertFunction::ToNonlinear); - } - DynamicImage::ImageRgba32F(y_axis) -} - -fn normalize(current_buffer: &mut ImageBuffer, Vec>, old_buffer: &ImageBuffer, Vec>, weight_sum: f64, rgba: (f64, f64, f64, f64), x: u32, y: u32) { - if weight_sum > 0. { - let r = (rgba.0 / weight_sum) as f32; - let g = (rgba.1 / weight_sum) as f32; - let b = (rgba.2 / weight_sum) as f32; - let a = (rgba.3 / weight_sum) as f32; - - current_buffer.put_pixel(x, y, Rgba([r, g, b, a])); - } else { - current_buffer.put_pixel(x, y, *old_buffer.get_pixel(x, y)); - } -} - // 1D gaussian kernel -fn create_gaussian_kernel(radius: f64) -> Vec { +fn gaussian_kernel(radius: f64) -> Vec { // Given radius, compute size of kernel -> 3*radius (approx.) let kernel_radius = (3. * radius).ceil() as usize; let kernel_size = 2 * kernel_radius + 1; @@ -180,7 +78,7 @@ fn create_gaussian_kernel(radius: f64) -> Vec { // Kernel values let two_radius_squared = 2. * radius * radius; - let sum: f64 = gaussian_kernel + let sum = gaussian_kernel .iter_mut() .enumerate() .map(|(i, value_at_index)| { @@ -189,7 +87,7 @@ fn create_gaussian_kernel(radius: f64) -> Vec { *value_at_index = exponent.exp(); *value_at_index }) - .sum(); + .sum::(); // Normalize gaussian_kernel.iter_mut().for_each(|value_at_index| *value_at_index /= sum); @@ -197,70 +95,110 @@ fn create_gaussian_kernel(radius: f64) -> Vec { gaussian_kernel } -fn box_blur(original_buffer: ImageBuffer, Vec>, radius: f64, nonlinear: bool) -> DynamicImage { +fn gaussian_blur_algorithm(mut original_buffer: ImageBuffer, Vec>, radius: f64, gamma: bool) -> DynamicImage { + if !gamma { + convert_color_space(&mut original_buffer, ConvertFunction::ToLinear) + } + let (width, height) = original_buffer.dimensions(); - let mut x_axis = ImageBuffer::new(width, height); - let mut blurred_image = ImageBuffer::new(width, height); - - // Blur along x-axis - for y in 0..height { - for x in 0..width { - let mut r_sum = 0.; - let mut g_sum = 0.; - let mut b_sum = 0.; - let mut a_sum = 0.; - let mut weight_sum = 0.; - - for dx in (x as i32 - radius as i32).max(0)..=(x as i32 + radius as i32).min(width as i32 - 1) { - let pixel = original_buffer.get_pixel(dx as u32, y); - let weight = 1.; - - r_sum += pixel[0] as f64 * weight; - g_sum += pixel[1] as f64 * weight; - b_sum += pixel[2] as f64 * weight; - a_sum += pixel[3] as f64 * weight; - weight_sum += weight; - } - x_axis.put_pixel( - x, - y, - Rgba([(r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32]), - ); + // 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 = ImageBuffer::, Vec>::new(width, height); + let mut y_axis = ImageBuffer::, Vec>::new(width, height); + + 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 { + let pixel = old_buffer.get_pixel([p as u32, x][pass], [y, p as u32][pass]); + + r_sum += pixel[0] as f64 * weight; + g_sum += pixel[1] as f64 * weight; + b_sum += pixel[2] as f64 * weight; + a_sum += pixel[3] as f64 * weight; + weight_sum += weight; + } + } + + // Normalize + let rgba = (r_sum, b_sum, g_sum, a_sum); + if weight_sum > 0. { + let r = (rgba.0 / weight_sum) as f32; + let g = (rgba.1 / weight_sum) as f32; + let b = (rgba.2 / weight_sum) as f32; + let a = (rgba.3 / weight_sum) as f32; + + current_buffer.put_pixel(x, y, Rgba([r, g, b, a])); + } else { + current_buffer.put_pixel(x, y, *old_buffer.get_pixel(x, y)); + } + } } } - // Blur along y-axis - for y in 0..height { - for x in 0..width { - let mut r_sum = 0.; - let mut g_sum = 0.; - let mut b_sum = 0.; - let mut a_sum = 0.; - let mut weight_sum = 0.; - - for dy in (y as i32 - radius as i32).max(0)..=(y as i32 + radius as i32).min(height as i32 - 1) { - let pixel = x_axis.get_pixel(x, dy as u32); - let weight = 1.; - - r_sum += pixel[0] as f64 * weight; - g_sum += pixel[1] as f64 * weight; - b_sum += pixel[2] as f64 * weight; - a_sum += pixel[3] as f64 * weight; - weight_sum += weight; - } + if !gamma { + convert_color_space(&mut y_axis, ConvertFunction::ToGamma); + } - blurred_image.put_pixel( - x, - y, - Rgba([(r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32]), - ); + DynamicImage::ImageRgba32F(y_axis) +} + +fn box_blur_algorithm(mut original_buffer: ImageBuffer, Vec>, radius: f64, gamma: bool) -> DynamicImage { + if !gamma { + convert_color_space(&mut original_buffer, ConvertFunction::ToLinear) + } + + let (width, height) = original_buffer.dimensions(); + let mut x_axis = ImageBuffer::new(width, height); + let mut y_axis = ImageBuffer::new(width, height); + + 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) { + let pixel = old_buffer.get_pixel([d as u32, x][pass], [y, d as u32][pass]); + let weight = 1.; + + r_sum += pixel[0] as f64 * weight; + g_sum += pixel[1] as f64 * weight; + b_sum += pixel[2] as f64 * weight; + a_sum += pixel[3] as f64 * weight; + weight_sum += weight; + } + + let pixel = Rgba([(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.put_pixel(x, y, pixel); + } } } - // Convert linear back to nonlinear if converted initially - if !nonlinear { - convert_color_space(&mut blurred_image, ConvertFunction::ToNonlinear); + if !gamma { + convert_color_space(&mut y_axis, ConvertFunction::ToGamma); } - DynamicImage::ImageRgba32F(blurred_image) + + DynamicImage::ImageRgba32F(y_axis) } From 286837fa22a8e44830fe80c2954a3dc05f5eb23f Mon Sep 17 00:00:00 2001 From: Calvin Vu Date: Sun, 20 Apr 2025 22:44:50 -0700 Subject: [PATCH 4/7] remove image crate, use conversion functions from color.rs --- node-graph/gstd/src/filter.rs | 116 +++++++++++++--------------------- 1 file changed, 43 insertions(+), 73 deletions(-) diff --git a/node-graph/gstd/src/filter.rs b/node-graph/gstd/src/filter.rs index 39d7af447e..3b8a2eb047 100644 --- a/node-graph/gstd/src/filter.rs +++ b/node-graph/gstd/src/filter.rs @@ -1,9 +1,8 @@ use graph_craft::proto::types::PixelLength; -use graphene_core::raster::Channel; use graphene_core::raster::image::{Image, ImageFrameTable}; +use graphene_core::raster::{Bitmap, BitmapMut}; use graphene_core::transform::{Transform, TransformMut}; use graphene_core::{Color, Ctx}; -use image::{DynamicImage, ImageBuffer, Rgba}; enum ConvertFunction { ToLinear, @@ -15,33 +14,19 @@ async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100 let image_frame_transform = image_frame.transform(); let image_frame_alpha_blending = image_frame.one_instance().alpha_blending; - let image = image_frame.one_instance().instance; - - // Prepare the image data for processing - let image_data = bytemuck::cast_vec(image.data.clone()); - let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type."); + let image = image_frame.one_instance().instance.clone(); // Run blur algorithm let blurred_image = if radius < 0.1 { // Minimum blur radius - image_buffer.into() + image.clone() } else if box_blur { - gaussian_blur_algorithm(image_buffer, radius, gamma) + gaussian_blur_algorithm(image, radius, gamma) } else { - box_blur_algorithm(image_buffer, radius, gamma) - }; - - // Prepare the image data for returning - let buffer = blurred_image.to_rgba32f().into_raw(); - let color_vec = bytemuck::cast_vec(buffer); - let processed_image = Image { - width: image.width, - height: image.height, - data: color_vec, - base64_string: None, + box_blur_algorithm(image, radius, gamma) }; - let mut result = ImageFrameTable::new(processed_image); + let mut result = ImageFrameTable::new(blurred_image); *result.transform_mut() = image_frame_transform; *result.one_instance_mut().alpha_blending = *image_frame_alpha_blending; @@ -49,23 +34,12 @@ async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100 } // Helper to convert image buffer to linear/nonlinear color spaces in-place -fn convert_color_space(image_buffer: &mut ImageBuffer, Vec>, convert: ConvertFunction) { - for pixel in image_buffer.pixels_mut() { - // Leave alpha channels - let channels = pixel.0; - - match convert { - ConvertFunction::ToLinear => { - pixel.0[0] = channels[0].to_linear(); - pixel.0[1] = channels[1].to_linear(); - pixel.0[2] = channels[2].to_linear(); - } - ConvertFunction::ToGamma => { - pixel.0[0] = Channel::from_linear(channels[0]); - pixel.0[1] = Channel::from_linear(channels[1]); - pixel.0[2] = Channel::from_linear(channels[2]); - } - } +fn convert_color_space(image: &mut Image, convert: ConvertFunction) { + for pixel in image.data.iter_mut() { + *pixel = match convert { + ConvertFunction::ToLinear => pixel.to_linear_srgb(), + ConvertFunction::ToGamma => pixel.to_gamma_srgb(), + }; } } @@ -95,7 +69,7 @@ fn gaussian_kernel(radius: f64) -> Vec { gaussian_kernel } -fn gaussian_blur_algorithm(mut original_buffer: ImageBuffer, Vec>, radius: f64, gamma: bool) -> DynamicImage { +fn gaussian_blur_algorithm(mut original_buffer: Image, radius: f64, gamma: bool) -> Image { if !gamma { convert_color_space(&mut original_buffer, ConvertFunction::ToLinear) } @@ -107,8 +81,8 @@ fn gaussian_blur_algorithm(mut original_buffer: ImageBuffer, Vec> let half_kernel = kernel.len() / 2; // Intermediate buffer for horizontal and vertical passes - let mut x_axis = ImageBuffer::, Vec>::new(width, height); - let mut y_axis = ImageBuffer::, Vec>::new(width, height); + 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 { @@ -125,28 +99,24 @@ fn gaussian_blur_algorithm(mut original_buffer: ImageBuffer, Vec> let p = [x, y][pass] as i32 + (i as i32 - half_kernel as i32); if p >= 0 && p < max as i32 { - let pixel = old_buffer.get_pixel([p as u32, x][pass], [y, p as u32][pass]); - - r_sum += pixel[0] as f64 * weight; - g_sum += pixel[1] as f64 * weight; - b_sum += pixel[2] as f64 * weight; - a_sum += pixel[3] as f64 * weight; - weight_sum += weight; + 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 rgba = (r_sum, b_sum, g_sum, a_sum); - if weight_sum > 0. { - let r = (rgba.0 / weight_sum) as f32; - let g = (rgba.1 / weight_sum) as f32; - let b = (rgba.2 / weight_sum) as f32; - let a = (rgba.3 / weight_sum) as f32; - - current_buffer.put_pixel(x, y, Rgba([r, g, b, a])); + 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 { - current_buffer.put_pixel(x, y, *old_buffer.get_pixel(x, y)); - } + 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)); } } } @@ -155,17 +125,17 @@ fn gaussian_blur_algorithm(mut original_buffer: ImageBuffer, Vec> convert_color_space(&mut y_axis, ConvertFunction::ToGamma); } - DynamicImage::ImageRgba32F(y_axis) + y_axis } -fn box_blur_algorithm(mut original_buffer: ImageBuffer, Vec>, radius: f64, gamma: bool) -> DynamicImage { +fn box_blur_algorithm(mut original_buffer: Image, radius: f64, gamma: bool) -> Image { if !gamma { convert_color_space(&mut original_buffer, ConvertFunction::ToLinear) } let (width, height) = original_buffer.dimensions(); - let mut x_axis = ImageBuffer::new(width, height); - let mut y_axis = ImageBuffer::new(width, height); + 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 { @@ -180,18 +150,18 @@ fn box_blur_algorithm(mut original_buffer: ImageBuffer, Vec>, rad 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) { - let pixel = old_buffer.get_pixel([d as u32, x][pass], [y, d as u32][pass]); - let weight = 1.; - - r_sum += pixel[0] as f64 * weight; - g_sum += pixel[1] as f64 * weight; - b_sum += pixel[2] as f64 * weight; - a_sum += pixel[3] as f64 * weight; - weight_sum += weight; + 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 pixel = Rgba([(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.put_pixel(x, y, pixel); + 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)); } } } @@ -200,5 +170,5 @@ fn box_blur_algorithm(mut original_buffer: ImageBuffer, Vec>, rad convert_color_space(&mut y_axis, ConvertFunction::ToGamma); } - DynamicImage::ImageRgba32F(y_axis) + y_axis } From a8b3a0e168a400b34a59e13cbf37da4a1241b1c1 Mon Sep 17 00:00:00 2001 From: Calvin Vu Date: Sun, 20 Apr 2025 23:52:48 -0700 Subject: [PATCH 5/7] fix box blur checkmark, fix linear/gamma conversion --- node-graph/gstd/src/filter.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/node-graph/gstd/src/filter.rs b/node-graph/gstd/src/filter.rs index 3b8a2eb047..ff6e052d8b 100644 --- a/node-graph/gstd/src/filter.rs +++ b/node-graph/gstd/src/filter.rs @@ -21,9 +21,9 @@ async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100 // Minimum blur radius image.clone() } else if box_blur { - gaussian_blur_algorithm(image, radius, gamma) - } else { box_blur_algorithm(image, radius, gamma) + } else { + gaussian_blur_algorithm(image, radius, gamma) }; let mut result = ImageFrameTable::new(blurred_image); @@ -70,8 +70,8 @@ fn gaussian_kernel(radius: f64) -> Vec { } fn gaussian_blur_algorithm(mut original_buffer: Image, radius: f64, gamma: bool) -> Image { - if !gamma { - convert_color_space(&mut original_buffer, ConvertFunction::ToLinear) + if gamma { + convert_color_space(&mut original_buffer, ConvertFunction::ToGamma) } let (width, height) = original_buffer.dimensions(); @@ -121,16 +121,16 @@ fn gaussian_blur_algorithm(mut original_buffer: Image, radius: f64, gamma } } - if !gamma { - convert_color_space(&mut y_axis, ConvertFunction::ToGamma); + if gamma { + convert_color_space(&mut y_axis, ConvertFunction::ToLinear); } y_axis } fn box_blur_algorithm(mut original_buffer: Image, radius: f64, gamma: bool) -> Image { - if !gamma { - convert_color_space(&mut original_buffer, ConvertFunction::ToLinear) + if gamma { + convert_color_space(&mut original_buffer, ConvertFunction::ToGamma) } let (width, height) = original_buffer.dimensions(); @@ -166,8 +166,8 @@ fn box_blur_algorithm(mut original_buffer: Image, radius: f64, gamma: boo } } - if !gamma { - convert_color_space(&mut y_axis, ConvertFunction::ToGamma); + if gamma { + convert_color_space(&mut y_axis, ConvertFunction::ToLinear); } y_axis From 8d047e74b99f03c2366fabe911e67ee5faf13c60 Mon Sep 17 00:00:00 2001 From: Calvin Vu Date: Thu, 24 Apr 2025 18:24:35 -0700 Subject: [PATCH 6/7] mult/unmult alpha before/after blur --- node-graph/gstd/src/filter.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/node-graph/gstd/src/filter.rs b/node-graph/gstd/src/filter.rs index ff6e052d8b..ebb2f87fbf 100644 --- a/node-graph/gstd/src/filter.rs +++ b/node-graph/gstd/src/filter.rs @@ -74,6 +74,7 @@ fn gaussian_blur_algorithm(mut original_buffer: Image, radius: f64, gamma convert_color_space(&mut original_buffer, ConvertFunction::ToGamma) } + original_buffer.map_pixels(|px| px.to_associated_alpha(px.a())); let (width, height) = original_buffer.dimensions(); // Create 1D gaussian kernel @@ -125,6 +126,7 @@ fn gaussian_blur_algorithm(mut original_buffer: Image, radius: f64, gamma convert_color_space(&mut y_axis, ConvertFunction::ToLinear); } + y_axis.map_pixels(|px| px.to_unassociated_alpha()); y_axis } @@ -133,6 +135,7 @@ fn box_blur_algorithm(mut original_buffer: Image, radius: f64, gamma: boo convert_color_space(&mut original_buffer, ConvertFunction::ToGamma) } + 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); @@ -170,5 +173,6 @@ fn box_blur_algorithm(mut original_buffer: Image, radius: f64, gamma: boo convert_color_space(&mut y_axis, ConvertFunction::ToLinear); } + y_axis.map_pixels(|px| px.to_unassociated_alpha()); y_axis } From 03d7f0821ac7d27cd8ef16b353946537b8174b99 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 29 Apr 2025 19:38:00 -0700 Subject: [PATCH 7/7] Code review --- node-graph/gstd/src/filter.rs | 53 ++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/node-graph/gstd/src/filter.rs b/node-graph/gstd/src/filter.rs index 81d3c214a2..dc08b1fba6 100644 --- a/node-graph/gstd/src/filter.rs +++ b/node-graph/gstd/src/filter.rs @@ -4,13 +4,20 @@ use graphene_core::raster::{Bitmap, BitmapMut}; use graphene_core::transform::{Transform, TransformMut}; use graphene_core::{Color, Ctx}; -enum ConvertFunction { - ToLinear, - ToGamma, -} - -#[node_macro::node(category("Raster"))] -async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100.))] radius: PixelLength, box_blur: bool, gamma: bool) -> ImageFrameTable { +/// 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, + /// 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 { let image_frame_transform = image_frame.transform(); let image_frame_alpha_blending = image_frame.one_instance_ref().alpha_blending; @@ -33,19 +40,9 @@ async fn blur(_: impl Ctx, image_frame: ImageFrameTable, #[range((0., 100 result } -// Helper to convert image buffer to linear/nonlinear color spaces in-place -fn convert_color_space(image: &mut Image, convert: ConvertFunction) { - for pixel in image.data.iter_mut() { - *pixel = match convert { - ConvertFunction::ToLinear => pixel.to_linear_srgb(), - ConvertFunction::ToGamma => pixel.to_gamma_srgb(), - }; - } -} - // 1D gaussian kernel fn gaussian_kernel(radius: f64) -> Vec { - // Given radius, compute size of kernel -> 3*radius (approx.) + // 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 = vec![0.; kernel_size]; @@ -71,10 +68,11 @@ fn gaussian_kernel(radius: f64) -> Vec { fn gaussian_blur_algorithm(mut original_buffer: Image, radius: f64, gamma: bool) -> Image { if gamma { - convert_color_space(&mut original_buffer, ConvertFunction::ToGamma) + 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())); } - original_buffer.map_pixels(|px| px.to_associated_alpha(px.a())); let (width, height) = original_buffer.dimensions(); // Create 1D gaussian kernel @@ -123,19 +121,21 @@ fn gaussian_blur_algorithm(mut original_buffer: Image, radius: f64, gamma } if gamma { - convert_color_space(&mut y_axis, ConvertFunction::ToLinear); + y_axis.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha()); + } else { + y_axis.map_pixels(|px| px.to_unassociated_alpha()); } - y_axis.map_pixels(|px| px.to_unassociated_alpha()); y_axis } fn box_blur_algorithm(mut original_buffer: Image, radius: f64, gamma: bool) -> Image { if gamma { - convert_color_space(&mut original_buffer, ConvertFunction::ToGamma) + 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())); } - 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); @@ -170,9 +170,10 @@ fn box_blur_algorithm(mut original_buffer: Image, radius: f64, gamma: boo } if gamma { - convert_color_space(&mut y_axis, ConvertFunction::ToLinear); + y_axis.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha()); + } else { + y_axis.map_pixels(|px| px.to_unassociated_alpha()); } - y_axis.map_pixels(|px| px.to_unassociated_alpha()); y_axis }