@@ -6,7 +6,7 @@ use raster_types::Image;
66use raster_types:: { Bitmap , BitmapMut } ;
77use raster_types:: { CPU , Raster } ;
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" ) ) ]
1111async 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