From 23b95b5e70dca62dae58e60efa670437f7b85773 Mon Sep 17 00:00:00 2001 From: Zadfar Date: Thu, 10 Sep 2026 20:09:19 +0530 Subject: [PATCH 1/6] add sharpen node --- node-graph/nodes/raster/src/filter.rs | 92 +++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index ac70828e770..6eab92a26c5 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -17,6 +17,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 { @@ -72,12 +83,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, @@ -148,6 +155,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 @@ -346,3 +389,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 0b9e0a7f08c13164054e7c6112081a314b22f31a Mon Sep 17 00:00:00 2001 From: Zadfar Zubair Date: Fri, 11 Sep 2026 02:42:40 +0530 Subject: [PATCH 2/6] 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 6eab92a26c5..133cdf289c1 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -391,13 +391,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; @@ -421,7 +441,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 e5c0a5909fc411bd2426f1ea8950668ac076f012 Mon Sep 17 00:00:00 2001 From: Zadfar Zubair Date: Fri, 11 Sep 2026 03:44:45 +0530 Subject: [PATCH 3/6] 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 133cdf289c1..97191cac5b1 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -433,7 +433,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 3a109559c43317df58c8297e35c65939a512e8eb Mon Sep 17 00:00:00 2001 From: Zadfar Zubair Date: Fri, 11 Sep 2026 17:17:59 +0530 Subject: [PATCH 4/6] 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 97191cac5b1..93198d4a342 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -59,13 +59,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 } @@ -220,7 +220,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 { @@ -234,7 +234,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 { @@ -400,21 +400,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 }); @@ -433,7 +419,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 From 7b1656dd3e562f1543679bf23885ae872d2dedb1 Mon Sep 17 00:00:00 2001 From: Zadfar Zubair Date: Sun, 13 Sep 2026 14:29:51 +0530 Subject: [PATCH 5/6] update to handle new color struct --- node-graph/nodes/raster/src/filter.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index 93198d4a342..5e4c1c44bca 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -427,8 +427,7 @@ 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); - 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); + *original = Color::from_gamma_srgb_channels(final_r, final_g, final_b, original_a); } buffer From e5495bc25208615301e0c5c8310f78240cbb7321 Mon Sep 17 00:00:00 2001 From: Zadfar Zubair Date: Tue, 29 Sep 2026 03:38:10 +0530 Subject: [PATCH 6/6] update node parameters --- node-graph/nodes/raster/src/filter.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index 5e4c1c44bca..acad478d356 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -162,20 +162,22 @@ async fn sharpen( /// The image to be sharpened. image_frame: Item>, /// The strength of the sharpening effect. + #[unit("%")] #[range] #[hard(0..)] #[soft(..100)] - amount: Item, + amount: Item, /// Sets how many pixels around edges are affected. + #[unit(" px")] #[range] #[hard(0..)] #[soft(..50)] - radius: Item, + radius: Item, /// Sets how many different pixels must be from surrounding area before sharpening is applied. #[range] #[hard(0..255)] #[soft(..30)] - threshold: Item, + threshold: Item, ) -> Item> { let (amount, radius, threshold) = (*amount.element(), *radius.element(), *threshold.element());