Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 97 additions & 11 deletions node-graph/nodes/raster/src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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 {
Expand Down Expand Up @@ -49,13 +60,13 @@ impl Alpha for PremultipliedGammaPixel {
}
}

fn premultiply_gamma(buffer: Image<Color>) -> Image<PremultipliedGammaPixel> {
fn premultiply_gamma(buffer: &Image<Color>) -> Image<PremultipliedGammaPixel> {
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 }
Expand All @@ -73,12 +84,8 @@ fn unpremultiply_gamma_to_linear(buffer: Image<PremultipliedGammaPixel>) -> 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,
Expand Down Expand Up @@ -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<Raster<CPU>>,
/// The strength of the sharpening effect.
#[range]
#[hard(0..)]
#[soft(..100)]
amount: Item<Percentage>,
/// Sets how many pixels around edges are affected.
#[range]
#[hard(0..)]
#[soft(..50)]
radius: Item<PixelLength>,
/// Sets how many different pixels must be from surrounding area before sharpening is applied.
#[range]
#[hard(0..255)]
#[soft(..30)]
threshold: Item<u32>,
) -> Item<Raster<CPU>> {
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<f64> {
// Given radius, compute the size of the kernel that's approximately three times the radius
Expand Down Expand Up @@ -172,7 +215,7 @@ fn gaussian_kernel(radius: f64) -> Vec<f64> {
fn gaussian_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
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 {
Expand All @@ -186,7 +229,7 @@ fn gaussian_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Im

fn box_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
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 {
Expand Down Expand Up @@ -341,3 +384,46 @@ 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<Color>, amount: f32, radius: f64, threshold: f32) -> Image<Color> {
// Normalize threshold and amount
let amount = amount / 100.;
let threshold = threshold / 255.;

if threshold >= 1. {
return buffer;
}

let kernel = gaussian_kernel(radius);
let working = premultiply_gamma(&buffer);

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;

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
}