Skip to content

Commit 606c186

Browse files
committed
Implementation of gaussian blur and box blur and raster category
1 parent dd27f46 commit 606c186

File tree

2 files changed

+244
-0
lines changed

2 files changed

+244
-0
lines changed

node-graph/gstd/src/filter.rs

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
use graph_craft::proto::types::PixelLength;
2+
use graphene_core::raster::image::{Image, ImageFrameTable};
3+
use graphene_core::transform::{Transform, TransformMut};
4+
use graphene_core::{Color, Ctx};
5+
use image::{DynamicImage, GenericImageView, ImageBuffer, Rgba};
6+
7+
#[node_macro::node(category("Raster"))]
8+
async fn blur(_: impl Ctx, image_frame: ImageFrameTable<Color>, #[range((0., 100.))] radius: PixelLength, gaussian_blur: bool) -> ImageFrameTable<Color> {
9+
let image_frame_transform = image_frame.transform();
10+
let image_frame_alpha_blending = image_frame.one_instance().alpha_blending;
11+
12+
let image = image_frame.one_instance().instance;
13+
14+
// Prepare the image data for processing
15+
let image_data = bytemuck::cast_vec(image.data.clone());
16+
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.");
17+
let dynamic_image: image::DynamicImage = image_buffer.into();
18+
19+
// Run blur algorithm
20+
let blurred_image = blur_helper(dynamic_image, radius, gaussian_blur);
21+
22+
// Prepare the image data for returning
23+
let buffer = blurred_image.to_rgba32f().into_raw();
24+
let color_vec = bytemuck::cast_vec(buffer);
25+
let processed_image = Image {
26+
width: image.width,
27+
height: image.height,
28+
data: color_vec,
29+
base64_string: None,
30+
};
31+
32+
let mut result = ImageFrameTable::new(processed_image);
33+
*result.transform_mut() = image_frame_transform;
34+
*result.one_instance_mut().alpha_blending = *image_frame_alpha_blending;
35+
36+
result
37+
}
38+
39+
fn blur_helper(image: DynamicImage, radius: f64, gaussian: bool) -> DynamicImage {
40+
// For small radius, image would not change much -> just return original image
41+
if radius < 1 as f64 {
42+
return image;
43+
} else {
44+
// Run the gaussian blur algorithm, if user wants
45+
if gaussian {
46+
return gaussian_blur(image, radius);
47+
}
48+
// Else, run box blur
49+
else {
50+
return box_blur(image, radius);
51+
}
52+
}
53+
}
54+
55+
fn gaussian_blur(image: DynamicImage, radius: f64) -> DynamicImage {
56+
let (width, height) = image.dimensions();
57+
let original_buffer = image.to_rgba8();
58+
59+
// Create 1D gaussian kernel
60+
let kernel = create_gaussian_kernel(radius);
61+
let half_kernel = kernel.len() / 2;
62+
63+
// Intermediate buffer for horizontal pass
64+
let mut x_axis = ImageBuffer::<Rgba<u8>, Vec<u8>>::new(width, height);
65+
// Blur along x-axis
66+
for y in 0..height {
67+
for x in 0..width {
68+
let mut r_sum = 0.0;
69+
let mut g_sum = 0.0;
70+
let mut b_sum = 0.0;
71+
let mut a_sum = 0.0;
72+
let mut weight_sum = 0.0;
73+
74+
for (i, &weight) in kernel.iter().enumerate() {
75+
let kx = i as i32 - half_kernel as i32;
76+
let px = x as i32 + kx;
77+
78+
if px >= 0 && px < width as i32 {
79+
let pixel = original_buffer.get_pixel(px as u32, y);
80+
81+
r_sum += pixel[0] as f64 * weight;
82+
g_sum += pixel[1] as f64 * weight;
83+
b_sum += pixel[2] as f64 * weight;
84+
a_sum += pixel[3] as f64 * weight;
85+
weight_sum += weight;
86+
}
87+
}
88+
89+
// Normalize
90+
if weight_sum > 0.0 {
91+
let r = (r_sum / weight_sum).clamp(0.0, 255.0) as u8;
92+
let g = (g_sum / weight_sum).clamp(0.0, 255.0) as u8;
93+
let b = (b_sum / weight_sum).clamp(0.0, 255.0) as u8;
94+
let a = (a_sum / weight_sum).clamp(0.0, 255.0) as u8;
95+
96+
x_axis.put_pixel(x, y, Rgba([r, g, b, a]));
97+
} else {
98+
x_axis.put_pixel(x, y, *original_buffer.get_pixel(x, y));
99+
}
100+
}
101+
}
102+
103+
// Intermediate buffer for vertical pass
104+
let mut y_axis = ImageBuffer::<Rgba<u8>, Vec<u8>>::new(width, height);
105+
// Blur along y-axis
106+
for y in 0..height {
107+
for x in 0..width {
108+
let mut r_sum = 0.0;
109+
let mut g_sum = 0.0;
110+
let mut b_sum = 0.0;
111+
let mut a_sum: f64 = 0.0;
112+
let mut weight_sum = 0.0;
113+
114+
for (i, &weight) in kernel.iter().enumerate() {
115+
let ky = i as i32 - half_kernel as i32;
116+
let py = y as i32 + ky;
117+
118+
if py >= 0 && py < height as i32 {
119+
let pixel = x_axis.get_pixel(x, py as u32);
120+
121+
r_sum += pixel[0] as f64 * weight;
122+
g_sum += pixel[1] as f64 * weight;
123+
b_sum += pixel[2] as f64 * weight;
124+
a_sum += pixel[3] as f64 * weight;
125+
weight_sum += weight;
126+
}
127+
}
128+
129+
if weight_sum > 0.0 {
130+
let r = (r_sum / weight_sum).clamp(0.0, 255.0) as u8;
131+
let g = (g_sum / weight_sum).clamp(0.0, 255.0) as u8;
132+
let b = (b_sum / weight_sum).clamp(0.0, 255.0) as u8;
133+
let a = (a_sum / weight_sum).clamp(0.0, 255.0) as u8;
134+
135+
y_axis.put_pixel(x, y, Rgba([r, g, b, a]));
136+
} else {
137+
y_axis.put_pixel(x, y, *x_axis.get_pixel(x, y));
138+
}
139+
}
140+
}
141+
142+
DynamicImage::ImageRgba8(y_axis)
143+
}
144+
145+
// 1D gaussian kernel
146+
fn create_gaussian_kernel(radius: f64) -> Vec<f64> {
147+
// Given radius, compute size of kernel -> 3*radius (approx.)
148+
let kernel_radius = (3.0 * radius).ceil() as usize;
149+
let kernel_size = 2 * kernel_radius + 1;
150+
let mut gaussian_kernel: Vec<f64> = vec![0.0; kernel_size];
151+
152+
// Kernel values
153+
let two_radius_squared = 2.0 * radius * radius;
154+
let mut sum = 0.0;
155+
for i in 0..kernel_size {
156+
let x: f64 = i as f64 - kernel_radius as f64;
157+
let exponent = -(x * x) / two_radius_squared;
158+
gaussian_kernel[i] = exponent.exp();
159+
sum += gaussian_kernel[i];
160+
}
161+
162+
// Normalize
163+
for i in 0..kernel_size {
164+
gaussian_kernel[i] /= sum;
165+
}
166+
167+
gaussian_kernel
168+
}
169+
170+
fn box_blur(image: DynamicImage, radius: f64) -> DynamicImage {
171+
let (width, height) = image.dimensions();
172+
let original_buffer = image.to_rgba8();
173+
let mut x_axis = ImageBuffer::new(width, height);
174+
let mut blurred_image = ImageBuffer::new(width, height);
175+
176+
// Blur along x-axis
177+
for y in 0..height {
178+
for x in 0..width {
179+
let mut r_sum = 0.0;
180+
let mut g_sum = 0.0;
181+
let mut b_sum = 0.0;
182+
let mut a_sum = 0.0;
183+
let mut weight_sum = 0.0;
184+
185+
for dx in (x as i32 - radius as i32).max(0)..=(x as i32 + radius as i32).min(width as i32 - 1) {
186+
let pixel = original_buffer.get_pixel(dx as u32, y);
187+
let weight = 1.0;
188+
189+
r_sum += pixel[0] as f64 * weight;
190+
g_sum += pixel[1] as f64 * weight;
191+
b_sum += pixel[2] as f64 * weight;
192+
a_sum += pixel[3] as f64 * weight;
193+
weight_sum += weight;
194+
}
195+
196+
x_axis.put_pixel(
197+
x,
198+
y,
199+
Rgba([
200+
(r_sum / weight_sum).round() as u8,
201+
(g_sum / weight_sum).round() as u8,
202+
(b_sum / weight_sum).round() as u8,
203+
(a_sum / weight_sum).round() as u8,
204+
]),
205+
);
206+
}
207+
}
208+
209+
// Blur along y-axis
210+
for y in 0..height {
211+
for x in 0..width {
212+
let mut r_sum = 0.0;
213+
let mut g_sum = 0.0;
214+
let mut b_sum = 0.0;
215+
let mut a_sum = 0.0;
216+
let mut weight_sum = 0.0;
217+
218+
for dy in (y as i32 - radius as i32).max(0)..=(y as i32 + radius as i32).min(height as i32 - 1) {
219+
let pixel = x_axis.get_pixel(x, dy as u32);
220+
let weight = 1.0;
221+
222+
r_sum += pixel[0] as f64 * weight;
223+
g_sum += pixel[1] as f64 * weight;
224+
b_sum += pixel[2] as f64 * weight;
225+
a_sum += pixel[3] as f64 * weight;
226+
weight_sum += weight;
227+
}
228+
229+
blurred_image.put_pixel(
230+
x,
231+
y,
232+
Rgba([
233+
(r_sum / weight_sum).round() as u8,
234+
(g_sum / weight_sum).round() as u8,
235+
(b_sum / weight_sum).round() as u8,
236+
(a_sum / weight_sum).round() as u8,
237+
]),
238+
);
239+
}
240+
}
241+
DynamicImage::ImageRgba8(blurred_image)
242+
}

node-graph/gstd/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ pub mod wasm_application_io;
3030
pub mod dehaze;
3131

3232
pub mod imaginate;
33+
34+
pub mod filter;

0 commit comments

Comments
 (0)