Skip to content

Commit 23b2c5b

Browse files
calvintvuKeavon
andauthored
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>
1 parent da38f67 commit 23b2c5b

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed

node-graph/gstd/src/filter.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use graph_craft::proto::types::PixelLength;
2+
use graphene_core::raster::image::{Image, ImageFrameTable};
3+
use graphene_core::raster::{Bitmap, BitmapMut};
4+
use graphene_core::transform::{Transform, TransformMut};
5+
use graphene_core::{Color, Ctx};
6+
7+
/// Blurs the image with a Gaussian or blur kernel filter.
8+
#[node_macro::node(category("Raster: Filter"))]
9+
async fn blur(
10+
_: impl Ctx,
11+
/// The image to be blurred.
12+
image_frame: ImageFrameTable<Color>,
13+
/// The radius of the blur kernel.
14+
#[range((0., 100.))]
15+
radius: PixelLength,
16+
/// Use a lower-quality box kernel instead of a circular Gaussian kernel. This is faster but produces boxy artifacts.
17+
box_blur: bool,
18+
/// Opt to incorrectly apply the filter with color calculations in gamma space for compatibility with the results from other software.
19+
gamma: bool,
20+
) -> ImageFrameTable<Color> {
21+
let image_frame_transform = image_frame.transform();
22+
let image_frame_alpha_blending = image_frame.one_instance_ref().alpha_blending;
23+
24+
let image = image_frame.one_instance_ref().instance.clone();
25+
26+
// Run blur algorithm
27+
let blurred_image = if radius < 0.1 {
28+
// Minimum blur radius
29+
image.clone()
30+
} else if box_blur {
31+
box_blur_algorithm(image, radius, gamma)
32+
} else {
33+
gaussian_blur_algorithm(image, radius, gamma)
34+
};
35+
36+
let mut result = ImageFrameTable::new(blurred_image);
37+
*result.transform_mut() = image_frame_transform;
38+
*result.one_instance_mut().alpha_blending = *image_frame_alpha_blending;
39+
40+
result
41+
}
42+
43+
// 1D gaussian kernel
44+
fn gaussian_kernel(radius: f64) -> Vec<f64> {
45+
// Given radius, compute the size of the kernel that's approximately three times the radius
46+
let kernel_radius = (3. * radius).ceil() as usize;
47+
let kernel_size = 2 * kernel_radius + 1;
48+
let mut gaussian_kernel: Vec<f64> = vec![0.; kernel_size];
49+
50+
// Kernel values
51+
let two_radius_squared = 2. * radius * radius;
52+
let sum = gaussian_kernel
53+
.iter_mut()
54+
.enumerate()
55+
.map(|(i, value_at_index)| {
56+
let x = i as f64 - kernel_radius as f64;
57+
let exponent = -(x * x) / two_radius_squared;
58+
*value_at_index = exponent.exp();
59+
*value_at_index
60+
})
61+
.sum::<f64>();
62+
63+
// Normalize
64+
gaussian_kernel.iter_mut().for_each(|value_at_index| *value_at_index /= sum);
65+
66+
gaussian_kernel
67+
}
68+
69+
fn gaussian_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
70+
if gamma {
71+
original_buffer.map_pixels(|px| px.to_gamma_srgb().to_associated_alpha(px.a()));
72+
} else {
73+
original_buffer.map_pixels(|px| px.to_associated_alpha(px.a()));
74+
}
75+
76+
let (width, height) = original_buffer.dimensions();
77+
78+
// Create 1D gaussian kernel
79+
let kernel = gaussian_kernel(radius);
80+
let half_kernel = kernel.len() / 2;
81+
82+
// Intermediate buffer for horizontal and vertical passes
83+
let mut x_axis = Image::new(width, height, Color::TRANSPARENT);
84+
let mut y_axis = Image::new(width, height, Color::TRANSPARENT);
85+
86+
for pass in [false, true] {
87+
let (max, old_buffer, current_buffer) = match pass {
88+
false => (width, &original_buffer, &mut x_axis),
89+
true => (height, &x_axis, &mut y_axis),
90+
};
91+
let pass = pass as usize;
92+
93+
for y in 0..height {
94+
for x in 0..width {
95+
let (mut r_sum, mut g_sum, mut b_sum, mut a_sum, mut weight_sum) = (0., 0., 0., 0., 0.);
96+
97+
for (i, &weight) in kernel.iter().enumerate() {
98+
let p = [x, y][pass] as i32 + (i as i32 - half_kernel as i32);
99+
100+
if p >= 0 && p < max as i32 {
101+
if let Some(px) = old_buffer.get_pixel([p as u32, x][pass], [y, p as u32][pass]) {
102+
r_sum += px.r() as f64 * weight;
103+
g_sum += px.g() as f64 * weight;
104+
b_sum += px.b() as f64 * weight;
105+
a_sum += px.a() as f64 * weight;
106+
weight_sum += weight;
107+
}
108+
}
109+
}
110+
111+
// Normalize
112+
let (r, g, b, a) = if weight_sum > 0. {
113+
((r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32)
114+
} else {
115+
let px = old_buffer.get_pixel(x, y).unwrap();
116+
(px.r(), px.g(), px.b(), px.a())
117+
};
118+
current_buffer.set_pixel(x, y, Color::from_rgbaf32_unchecked(r, g, b, a));
119+
}
120+
}
121+
}
122+
123+
if gamma {
124+
y_axis.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha());
125+
} else {
126+
y_axis.map_pixels(|px| px.to_unassociated_alpha());
127+
}
128+
129+
y_axis
130+
}
131+
132+
fn box_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
133+
if gamma {
134+
original_buffer.map_pixels(|px| px.to_gamma_srgb().to_associated_alpha(px.a()));
135+
} else {
136+
original_buffer.map_pixels(|px| px.to_associated_alpha(px.a()));
137+
}
138+
139+
let (width, height) = original_buffer.dimensions();
140+
let mut x_axis = Image::new(width, height, Color::TRANSPARENT);
141+
let mut y_axis = Image::new(width, height, Color::TRANSPARENT);
142+
143+
for pass in [false, true] {
144+
let (max, old_buffer, current_buffer) = match pass {
145+
false => (width, &original_buffer, &mut x_axis),
146+
true => (height, &x_axis, &mut y_axis),
147+
};
148+
let pass = pass as usize;
149+
150+
for y in 0..height {
151+
for x in 0..width {
152+
let (mut r_sum, mut g_sum, mut b_sum, mut a_sum, mut weight_sum) = (0., 0., 0., 0., 0.);
153+
154+
let i = [x, y][pass];
155+
for d in (i as i32 - radius as i32).max(0)..=(i as i32 + radius as i32).min(max as i32 - 1) {
156+
if let Some(px) = old_buffer.get_pixel([d as u32, x][pass], [y, d as u32][pass]) {
157+
let weight = 1.;
158+
r_sum += px.r() as f64 * weight;
159+
g_sum += px.g() as f64 * weight;
160+
b_sum += px.b() as f64 * weight;
161+
a_sum += px.a() as f64 * weight;
162+
weight_sum += weight;
163+
}
164+
}
165+
166+
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);
167+
current_buffer.set_pixel(x, y, Color::from_rgbaf32_unchecked(r, g, b, a));
168+
}
169+
}
170+
}
171+
172+
if gamma {
173+
y_axis.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha());
174+
} else {
175+
y_axis.map_pixels(|px| px.to_unassociated_alpha());
176+
}
177+
178+
y_axis
179+
}

node-graph/gstd/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod vector;
88
pub use graphene_core::*;
99
pub mod brush;
1010
pub mod dehaze;
11+
pub mod filter;
1112
pub mod image_color_palette;
1213
#[cfg(feature = "wasm")]
1314
pub mod wasm_application_io;

0 commit comments

Comments
 (0)