From 8d312481826b2579df0f0e89cc033ee709a491a3 Mon Sep 17 00:00:00 2001 From: Zadfar Date: Thu, 10 Sep 2026 20:09:19 +0530 Subject: [PATCH 1/4] add sharpen node --- node-graph/nodes/raster/src/filter.rs | 94 +++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index bfe2001f2ee..74898fdc24b 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -2,7 +2,7 @@ use bytemuck::{Pod, Zeroable}; use core_types::color::{Alpha, Color, Pixel, RGB}; use core_types::context::Ctx; use core_types::list::Item; -use core_types::registry::types::PixelLength; +use core_types::registry::types::{Percentage, PixelLength}; use raster_types::Image; use raster_types::{Bitmap, BitmapMut}; use raster_types::{CPU, Raster}; @@ -18,6 +18,17 @@ struct PremultipliedGammaPixel { a: f32, } +impl PremultipliedGammaPixel { + fn to_unpremultiplied_channels(self) -> [f32; 4] { + if self.a > 0. { + let inv_a = 1. / self.a; + [self.r * inv_a, self.g * inv_a, self.b * inv_a, self.a] + } else { + [0., 0., 0., 0.] + } + } +} + impl Pixel for PremultipliedGammaPixel {} impl RGB for PremultipliedGammaPixel { @@ -73,12 +84,8 @@ fn unpremultiply_gamma_to_linear(buffer: Image) -> Imag .data .into_iter() .map(|px| { - if px.a > 0. { - let inv_a = 1. / px.a; - Color::from_gamma_srgb_channels(px.r * inv_a, px.g * inv_a, px.b * inv_a, px.a) - } else { - Color::TRANSPARENT - } + let [r, g, b, a] = px.to_unpremultiplied_channels(); + Color::from_gamma_srgb_channels(r, g, b, a) }) .collect(), base64_string: None, @@ -143,6 +150,42 @@ async fn median_filter( Item::from_parts(filtered_image, attributes) } +/// Sharpens the image using unsharp mask. +#[node_macro::node(category("Raster: Filter"))] +async fn sharpen( + _: impl Ctx, + /// The image to be sharpened. + image_frame: Item>, + /// The strength of the sharpening effect. + #[range] + #[hard(0..)] + #[soft(..100)] + amount: Item, + /// Sets how many pixels around edges are affected. + #[range] + #[hard(0..)] + #[soft(..50)] + radius: Item, + /// Sets how many different pixels must be from surrounding area before sharpening is applied. + #[range] + #[hard(0..255)] + #[soft(..30)] + threshold: Item, +) -> Item> { + let (amount, radius, threshold) = (*amount.element(), *radius.element(), *threshold.element()); + + let (image, attributes) = image_frame.into_parts(); + + let sharpened_image = if radius < 0.1 || amount == 0. { + // Minimum sharpen radius and amount + image + } else { + Raster::new_cpu(sharpen_algorithm(image.into_data(), amount as f32, radius, threshold as f32)) + }; + + Item::from_parts(sharpened_image, attributes) +} + // 1D gaussian kernel fn gaussian_kernel(radius: f64) -> Vec { // Given radius, compute the size of the kernel that's approximately three times the radius @@ -341,3 +384,40 @@ fn median_quickselect(values: &mut [f32]) -> f32 { // Use total_cmp for safe NaN handling instead of partial_cmp().unwrap() *values.select_nth_unstable_by(mid, |a, b| a.total_cmp(b)).1 } + +fn sharpen_algorithm(mut buffer: Image, amount: f32, radius: f64, threshold: f32) -> Image { + let kernel = gaussian_kernel(radius); + let working = premultiply_gamma(buffer.clone()); + let blurred_image = gaussian_separable(working, &kernel, |r, g, b, a| PremultipliedGammaPixel { r, g, b, a }); + + // Normalize threshold and amount + let amount = amount / 100.; + let threshold = threshold / 255.; + // Width of the linear transition around the threshold + let threshold_fade_width = threshold * 0.75; + + let sharpen_channel = |orig: f32, blur: f32| -> f32 { + // This operates on normalized sRGB values + let diff = orig - blur; + let mask = if threshold_fade_width > 0.0 { + ((diff.abs() - threshold + threshold_fade_width) / (threshold_fade_width * 2.)).clamp(0., 1.) + } else { + 1.0 + }; + (orig + diff * amount * mask).clamp(0., 1.) + }; + + for (original, blurred) in buffer.data.iter_mut().zip(&blurred_image.data) { + let [original_r, original_g, original_b, original_a] = original.to_gamma_srgb_channels(); + let [blurred_r, blurred_g, blurred_b, _] = blurred.to_unpremultiplied_channels(); + + // Sharpens RGB channels while preserving alpha channel + let final_r = sharpen_channel(original_r, blurred_r); + let final_g = sharpen_channel(original_g, blurred_g); + let final_b = sharpen_channel(original_b, blurred_b); + + *original = Color::from_gamma_srgb_channels(final_r, final_g, final_b, original_a); + } + + buffer +} From 9e675aea7f928873ccdf8322e238300b8f49fdde Mon Sep 17 00:00:00 2001 From: Zadfar Zubair Date: Fri, 11 Sep 2026 02:42:40 +0530 Subject: [PATCH 2/4] fixed threshold and buffer allocation --- node-graph/nodes/raster/src/filter.rs | 31 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index 74898fdc24b..8520e179c21 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -386,13 +386,33 @@ fn median_quickselect(values: &mut [f32]) -> f32 { } fn sharpen_algorithm(mut buffer: Image, amount: f32, radius: f64, threshold: f32) -> Image { - let kernel = gaussian_kernel(radius); - let working = premultiply_gamma(buffer.clone()); - let blurred_image = gaussian_separable(working, &kernel, |r, g, b, a| PremultipliedGammaPixel { r, g, b, a }); - // Normalize threshold and amount let amount = amount / 100.; let threshold = threshold / 255.; + + if threshold >= 1. { + return buffer; + } + + let kernel = gaussian_kernel(radius); + + let working_data = buffer + .data + .iter() + .map(|c| { + let [r, g, b, a] = c.to_gamma_srgb_channels(); + PremultipliedGammaPixel { r: r * a, g: g * a, b: b * a, a } + }) + .collect(); + let working = Image { + width: buffer.width, + height: buffer.height, + data: working_data, + base64_string: None, + }; + + let blurred_image = gaussian_separable(working, &kernel, |r, g, b, a| PremultipliedGammaPixel { r, g, b, a }); + // Width of the linear transition around the threshold let threshold_fade_width = threshold * 0.75; @@ -416,7 +436,8 @@ fn sharpen_algorithm(mut buffer: Image, amount: f32, radius: f64, thresho let final_g = sharpen_channel(original_g, blurred_g); let final_b = sharpen_channel(original_b, blurred_b); - *original = Color::from_gamma_srgb_channels(final_r, final_g, final_b, original_a); + let unassociated = Color::from_gamma_srgb_channels(final_r, final_g, final_b, original_a); + *original = Color::from_rgbaf32_unchecked(unassociated.r() * original_a, unassociated.g() * original_a, unassociated.b() * original_a, original_a); } buffer From 3b779f4bbad285ee18cfcae29675a3939e44d9c4 Mon Sep 17 00:00:00 2001 From: Zadfar Zubair Date: Fri, 11 Sep 2026 03:44:45 +0530 Subject: [PATCH 3/4] fixed alpha handling --- node-graph/nodes/raster/src/filter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index 8520e179c21..dd1df1b97ec 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -428,7 +428,7 @@ fn sharpen_algorithm(mut buffer: Image, amount: f32, radius: f64, thresho }; for (original, blurred) in buffer.data.iter_mut().zip(&blurred_image.data) { - let [original_r, original_g, original_b, original_a] = original.to_gamma_srgb_channels(); + let [original_r, original_g, original_b, original_a] = original.to_unassociated_alpha().to_gamma_srgb_channels(); let [blurred_r, blurred_g, blurred_b, _] = blurred.to_unpremultiplied_channels(); // Sharpens RGB channels while preserving alpha channel From ba8466d5e2d351fc15d5a96a7ba49d0d73411e67 Mon Sep 17 00:00:00 2001 From: Zadfar Zubair Date: Fri, 11 Sep 2026 17:17:59 +0530 Subject: [PATCH 4/4] revert alpha handling and optimize premultiply gamma --- node-graph/nodes/raster/src/filter.rs | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index dd1df1b97ec..b7fe361cfa4 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -60,13 +60,13 @@ impl Alpha for PremultipliedGammaPixel { } } -fn premultiply_gamma(buffer: Image) -> Image { +fn premultiply_gamma(buffer: &Image) -> Image { Image { width: buffer.width, height: buffer.height, data: buffer .data - .into_iter() + .iter() .map(|px| { let [r, g, b, a] = px.to_gamma_srgb_channels(); PremultipliedGammaPixel { r: r * a, g: g * a, b: b * a, a } @@ -215,7 +215,7 @@ fn gaussian_kernel(radius: f64) -> Vec { fn gaussian_blur_algorithm(buffer: Image, radius: f64, gamma: bool) -> Image { let kernel = gaussian_kernel(radius); if gamma { - let working = premultiply_gamma(buffer); + let working = premultiply_gamma(&buffer); let blurred = gaussian_separable(working, &kernel, |r, g, b, a| PremultipliedGammaPixel { r, g, b, a }); unpremultiply_gamma_to_linear(blurred) } else { @@ -229,7 +229,7 @@ fn gaussian_blur_algorithm(buffer: Image, radius: f64, gamma: bool) -> Im fn box_blur_algorithm(buffer: Image, radius: f64, gamma: bool) -> Image { if gamma { - let working = premultiply_gamma(buffer); + let working = premultiply_gamma(&buffer); let blurred = box_separable(working, radius, |r, g, b, a| PremultipliedGammaPixel { r, g, b, a }); unpremultiply_gamma_to_linear(blurred) } else { @@ -395,21 +395,7 @@ fn sharpen_algorithm(mut buffer: Image, amount: f32, radius: f64, thresho } let kernel = gaussian_kernel(radius); - - let working_data = buffer - .data - .iter() - .map(|c| { - let [r, g, b, a] = c.to_gamma_srgb_channels(); - PremultipliedGammaPixel { r: r * a, g: g * a, b: b * a, a } - }) - .collect(); - let working = Image { - width: buffer.width, - height: buffer.height, - data: working_data, - base64_string: None, - }; + let working = premultiply_gamma(&buffer); let blurred_image = gaussian_separable(working, &kernel, |r, g, b, a| PremultipliedGammaPixel { r, g, b, a }); @@ -428,7 +414,7 @@ fn sharpen_algorithm(mut buffer: Image, amount: f32, radius: f64, thresho }; for (original, blurred) in buffer.data.iter_mut().zip(&blurred_image.data) { - let [original_r, original_g, original_b, original_a] = original.to_unassociated_alpha().to_gamma_srgb_channels(); + let [original_r, original_g, original_b, original_a] = original.to_gamma_srgb_channels(); let [blurred_r, blurred_g, blurred_b, _] = blurred.to_unpremultiplied_channels(); // Sharpens RGB channels while preserving alpha channel