Skip to content

Commit ab88c3f

Browse files
committed
Add median filter option to blur node:
- Add median parameter to blur function for noise reduction - Implement median_filter_algorithm with efficient quickselect - Support gamma space calculations for median filtering - Preserve edges while removing noise, complementing existing blur options Feature in Issue: #912
1 parent edc018d commit ab88c3f

File tree

1 file changed

+63
-2
lines changed

1 file changed

+63
-2
lines changed

node-graph/graster-nodes/src/filter.rs

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use graphene_core::raster_types::{CPU, Raster};
66
use graphene_core::registry::types::PixelLength;
77
use graphene_core::table::Table;
88

9-
/// Blurs the image with a Gaussian or blur kernel filter.
9+
/// Blurs the image with a Gaussian, blur kernel or Median filter.
1010
#[node_macro::node(category("Raster: Filter"))]
1111
async fn blur(
1212
_: impl Ctx,
@@ -18,6 +18,8 @@ async fn blur(
1818
radius: PixelLength,
1919
/// Use a lower-quality box kernel instead of a circular Gaussian kernel. This is faster but produces boxy artifacts.
2020
box_blur: bool,
21+
/// Use a median filter instead of a blur. This is good for removing noise while preserving edges, but does not produce a smooth blur effect.
22+
median: bool,
2123
/// Opt to incorrectly apply the filter with color calculations in gamma space for compatibility with the results from other software.
2224
gamma: bool,
2325
) -> Table<Raster<CPU>> {
@@ -32,7 +34,10 @@ async fn blur(
3234
image.clone()
3335
} else if box_blur {
3436
Raster::new_cpu(box_blur_algorithm(image.into_data(), radius, gamma))
35-
} else {
37+
} else if median {
38+
Raster::new_cpu(median_filter_algorithm(image.into_data(), radius as u32, gamma))
39+
}
40+
else {
3641
Raster::new_cpu(gaussian_blur_algorithm(image.into_data(), radius, gamma))
3742
};
3843

@@ -179,3 +184,59 @@ fn box_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: boo
179184

180185
y_axis
181186
}
187+
188+
fn median_filter_algorithm(mut original_buffer: Image<Color>, radius: u32, gamma: bool) -> Image<Color> {
189+
if gamma {
190+
original_buffer.map_pixels(|px| px.to_gamma_srgb().to_associated_alpha(px.a()));
191+
} else {
192+
original_buffer.map_pixels(|px| px.to_associated_alpha(px.a()));
193+
}
194+
195+
let (width, height) = original_buffer.dimensions();
196+
let mut output = Image::new(width, height, Color::TRANSPARENT);
197+
198+
for y in 0..height {
199+
for x in 0..width {
200+
// Collect pixel neighborhood
201+
let mut r_vals = Vec::with_capacity(((2 * radius + 1).pow(2)) as usize);
202+
let mut g_vals = Vec::with_capacity(r_vals.capacity());
203+
let mut b_vals = Vec::with_capacity(r_vals.capacity());
204+
let mut a_vals = Vec::with_capacity(r_vals.capacity());
205+
206+
for ny in y.saturating_sub(radius)..=(y + radius).min(height - 1) {
207+
for nx in x.saturating_sub(radius)..=(x + radius).min(width - 1) {
208+
if let Some(px) = original_buffer.get_pixel(nx, ny) {
209+
r_vals.push(px.r());
210+
g_vals.push(px.g());
211+
b_vals.push(px.b());
212+
a_vals.push(px.a());
213+
}
214+
}
215+
}
216+
217+
// Use quickselect instead of sorting for efficiency
218+
let r = median_quickselect(&mut r_vals);
219+
let g = median_quickselect(&mut g_vals);
220+
let b = median_quickselect(&mut b_vals);
221+
let a = median_quickselect(&mut a_vals);
222+
223+
output.set_pixel(x, y, Color::from_rgbaf32_unchecked(r, g, b, a));
224+
}
225+
}
226+
227+
if gamma {
228+
output.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha());
229+
} else {
230+
output.map_pixels(|px| px.to_unassociated_alpha());
231+
}
232+
233+
output
234+
}
235+
236+
/// Finds the median of a slice using quickselect for efficiency.
237+
/// This avoids the cost of full sorting (O(n log n)).
238+
fn median_quickselect(values: &mut [f32]) -> f32 {
239+
let mid: usize = values.len() / 2;
240+
// nth_unstable is like quickselect: average O(n)
241+
*values.select_nth_unstable_by(mid, |a, b| a.partial_cmp(b).unwrap()).1
242+
}

0 commit comments

Comments
 (0)