scirs2_signal/
median.rs

1use scirs2_core::ndarray::s;
2// Median Filtering module
3//
4// This module implements median filtering techniques for signal and image processing.
5// Median filtering is particularly effective at removing salt-and-pepper and
6// impulse noise while preserving edges.
7//
8// The implementation includes:
9// - 1D Median filtering for signals
10// - 2D Median filtering for images
11// - Weighted median filtering
12// - Adaptive median filtering
13// - Edge-preserving median filtering variants
14//
15// # Example
16// ```
17// use scirs2_core::ndarray::Array1;
18// use scirs2signal::median::{median_filter_1d, MedianConfig};
19//
20// // Create a test signal with impulse noise
21// let mut signal = Array1::from_vec(vec![1.0, 1.2, 1.1, 5.0, 1.3, 1.2, 0.0, 1.1]);
22//
23// // Apply median filter with window size 3
24// let config = MedianConfig::default();
25// let filtered = median_filter_1d(&signal, 3, &config).unwrap();
26// // The outliers (5.0 and 0.0) will be replaced with median values
27// ```
28
29use crate::error::{SignalError, SignalResult};
30use scirs2_core::ndarray::{Array1, Array2, Array3, Axis};
31
32#[allow(unused_imports)]
33/// Edge handling mode for median filtering
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub enum EdgeMode {
36    /// Reflect the signal at boundaries
37    Reflect,
38
39    /// Pad with the nearest valid value
40    Nearest,
41
42    /// Pad with zeros
43    Constant(f64),
44
45    /// Wrap around (circular padding)
46    Wrap,
47}
48
49/// Configuration for median filtering
50#[derive(Debug, Clone)]
51pub struct MedianConfig {
52    /// Edge handling mode
53    pub edge_mode: EdgeMode,
54
55    /// Whether to use adaptive kernel size
56    pub adaptive: bool,
57
58    /// Maximum kernel size for adaptive filtering
59    pub max_kernel_size: usize,
60
61    /// Noise threshold for adaptive filtering
62    pub noise_threshold: f64,
63
64    /// Whether to apply center weighted median filtering
65    pub center_weighted: bool,
66
67    /// Center weight factor (higher values give more weight to the center pixel)
68    pub center_weight: usize,
69}
70
71impl Default for MedianConfig {
72    fn default() -> Self {
73        Self {
74            edge_mode: EdgeMode::Reflect,
75            adaptive: false,
76            max_kernel_size: 9,
77            noise_threshold: 50.0,
78            center_weighted: false,
79            center_weight: 3,
80        }
81    }
82}
83
84/// Applies median filtering to a 1D signal.
85///
86/// Median filtering replaces each value with the median of neighboring values,
87/// which is effective at removing outliers and impulse noise.
88///
89/// # Arguments
90/// * `signal` - Input signal
91/// * `kernel_size` - Size of the filtering window (must be odd)
92/// * `config` - Filtering configuration
93///
94/// # Returns
95/// * The filtered signal
96///
97/// # Example
98/// ```
99/// use scirs2_core::ndarray::Array1;
100/// use scirs2_signal::median::{median_filter_1d, MedianConfig};
101///
102/// let signal = Array1::from_vec(vec![1.0, 1.2, 5.0, 1.1, 1.3, 0.0, 1.2]);
103/// let config = MedianConfig::default();
104/// let filtered = median_filter_1d(&signal, 3, &config).unwrap();
105/// ```
106#[allow(dead_code)]
107pub fn median_filter_1d(
108    signal: &Array1<f64>,
109    kernel_size: usize,
110    config: &MedianConfig,
111) -> SignalResult<Array1<f64>> {
112    // Validate kernel _size
113    if kernel_size % 2 != 1 {
114        return Err(SignalError::ValueError(
115            "Kernel _size must be odd".to_string(),
116        ));
117    }
118
119    let n = signal.len();
120
121    // If signal is too short, return a copy
122    if n <= 1 || kernel_size > n {
123        return Ok(signal.clone());
124    }
125
126    let half_kernel = kernel_size / 2;
127
128    // Create padded signal based on edge mode
129    let paddedsignal = padsignal_1d(signal, half_kernel, config.edge_mode);
130
131    // Apply either standard or adaptive median filtering
132    if config.adaptive {
133        adaptive_median_filter_1d(signal, &paddedsignal, half_kernel, config)
134    } else if config.center_weighted {
135        center_weighted_median_filter_1d(signal, &paddedsignal, half_kernel, config)
136    } else {
137        standard_median_filter_1d(signal, &paddedsignal, half_kernel)
138    }
139}
140
141/// Applies standard median filtering to a 1D signal
142#[allow(dead_code)]
143fn standard_median_filter_1d(
144    signal: &Array1<f64>,
145    paddedsignal: &Array1<f64>,
146    half_kernel: usize,
147) -> SignalResult<Array1<f64>> {
148    let n = signal.len();
149    let mut filtered = Array1::zeros(n);
150
151    // Process each point in the signal
152    for i in 0..n {
153        // Extract window around current point
154        let window_start = i;
155        let window_end = i + 2 * half_kernel + 1;
156
157        // Ensure window is within bounds
158        if window_start >= paddedsignal.len() || window_end > paddedsignal.len() {
159            return Err(SignalError::DimensionMismatch(
160                "Window extends beyond padded signal bounds".to_string(),
161            ));
162        }
163
164        // Extract and sort window values
165        let mut window: Vec<f64> = paddedsignal.slice(s![window_start..window_end]).to_vec();
166        window.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
167
168        // Set output to median value
169        filtered[i] = window[half_kernel];
170    }
171
172    Ok(filtered)
173}
174
175/// Applies center-weighted median filtering to a 1D signal
176#[allow(dead_code)]
177fn center_weighted_median_filter_1d(
178    signal: &Array1<f64>,
179    paddedsignal: &Array1<f64>,
180    half_kernel: usize,
181    config: &MedianConfig,
182) -> SignalResult<Array1<f64>> {
183    let n = signal.len();
184    let mut filtered = Array1::zeros(n);
185
186    // Process each point in the signal
187    for i in 0..n {
188        // Extract window around current point
189        let window_start = i;
190        let window_end = i + 2 * half_kernel + 1;
191
192        // Ensure window is within bounds
193        if window_start >= paddedsignal.len() || window_end > paddedsignal.len() {
194            return Err(SignalError::DimensionMismatch(
195                "Window extends beyond padded signal bounds".to_string(),
196            ));
197        }
198
199        // Create weighted window by repeating the center value
200        let mut weighted_window = Vec::new();
201
202        for j in window_start..window_end {
203            let value = paddedsignal[j];
204
205            // Add center value with higher weight
206            if j == window_start + half_kernel {
207                for _ in 0..config.center_weight {
208                    weighted_window.push(value);
209                }
210            } else {
211                weighted_window.push(value);
212            }
213        }
214
215        // Sort the weighted window
216        weighted_window.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
217
218        // Calculate the median position (considering the weighted values)
219        let median_idx = weighted_window.len() / 2;
220
221        // Set output to weighted median value
222        filtered[i] = weighted_window[median_idx];
223    }
224
225    Ok(filtered)
226}
227
228/// Applies adaptive median filtering to a 1D signal
229#[allow(dead_code)]
230fn adaptive_median_filter_1d(
231    signal: &Array1<f64>,
232    paddedsignal: &Array1<f64>,
233    initial_half_kernel: usize,
234    config: &MedianConfig,
235) -> SignalResult<Array1<f64>> {
236    let n = signal.len();
237    let mut filtered = Array1::zeros(n);
238
239    // Maximum half _kernel size
240    let max_half_kernel = config.max_kernel_size / 2;
241
242    // Process each point in the signal
243    for i in 0..n {
244        // Start with the initial _kernel size
245        let mut half_kernel = initial_half_kernel;
246        let mut window_size = 2 * half_kernel + 1;
247
248        // Extract the current pixel value
249        let curr_val = paddedsignal[i + half_kernel];
250
251        // Adaptive window size adjustment
252        while half_kernel <= max_half_kernel {
253            // Extract window around current point
254            let window_start = i + (initial_half_kernel - half_kernel);
255            let window_end = window_start + window_size;
256
257            // Ensure window is within bounds
258            if window_end > paddedsignal.len() {
259                break;
260            }
261
262            // Extract and sort window values
263            let window: Vec<f64> = paddedsignal.slice(s![window_start..window_end]).to_vec();
264            let mut sorted_window = window.clone();
265            sorted_window.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
266
267            // Calculate window statistics
268            let median = sorted_window[half_kernel];
269            let min_val = sorted_window[0];
270            let max_val = sorted_window[sorted_window.len() - 1];
271
272            // Level A: Test if median is impulse
273            let level_a = min_val < median && median < max_val;
274
275            if level_a {
276                // Level B: Test if current pixel is impulse
277                let level_b = min_val < curr_val && curr_val < max_val;
278
279                if level_b {
280                    // Not an impulse, keep original value
281                    filtered[i] = curr_val;
282                } else {
283                    // Impulse detected, use median
284                    filtered[i] = median;
285                }
286
287                // Exit the window size adjustment loop
288                break;
289            } else {
290                // Median might be impulse, increase window size
291                half_kernel += 1;
292                window_size = 2 * half_kernel + 1;
293
294                // If we've reached the maximum window size, use the median
295                if half_kernel > max_half_kernel {
296                    filtered[i] = median;
297                }
298            }
299        }
300    }
301
302    Ok(filtered)
303}
304
305/// Applies median filtering to a 2D image.
306///
307/// Median filtering is particularly effective at removing salt-and-pepper noise
308/// from images while preserving edges.
309///
310/// # Arguments
311/// * `image` - Input image (2D array)
312/// * `kernel_size` - Size of the filtering window (must be odd)
313/// * `config` - Filtering configuration
314///
315/// # Returns
316/// * The filtered image
317///
318/// # Example
319/// ```
320/// use scirs2_core::ndarray::Array2;
321/// use scirs2_signal::median::{median_filter_2d, MedianConfig};
322///
323/// let image = Array2::from_shape_fn((5, 5), |(i, j)| {
324///     if i == 2 && j == 2 { 100.0 } else { 1.0 }  // Center pixel is an outlier
325/// });
326/// let config = MedianConfig::default();
327/// let filtered = median_filter_2d(&image, 3, &config).unwrap();
328/// ```
329#[allow(dead_code)]
330pub fn median_filter_2d(
331    image: &Array2<f64>,
332    kernel_size: usize,
333    config: &MedianConfig,
334) -> SignalResult<Array2<f64>> {
335    // Validate kernel _size
336    if kernel_size % 2 != 1 {
337        return Err(SignalError::ValueError(
338            "Kernel _size must be odd".to_string(),
339        ));
340    }
341
342    let (height, width) = image.dim();
343
344    // If image is too small, return a copy
345    if height <= 1 || width <= 1 || kernel_size > height || kernel_size > width {
346        return Ok(image.clone());
347    }
348
349    let half_kernel = kernel_size / 2;
350
351    // Create padded image based on edge mode
352    let paddedimage = padimage_2d(image, half_kernel, config.edge_mode);
353
354    // Apply either standard or adaptive median filtering
355    if config.adaptive {
356        adaptive_median_filter_2d(image, &paddedimage, half_kernel, config)
357    } else if config.center_weighted {
358        center_weighted_median_filter_2d(image, &paddedimage, half_kernel, config)
359    } else {
360        standard_median_filter_2d(image, &paddedimage, half_kernel)
361    }
362}
363
364/// Applies standard median filtering to a 2D image
365#[allow(dead_code)]
366fn standard_median_filter_2d(
367    image: &Array2<f64>,
368    paddedimage: &Array2<f64>,
369    half_kernel: usize,
370) -> SignalResult<Array2<f64>> {
371    let (height, width) = image.dim();
372    let mut filtered = Array2::zeros((height, width));
373
374    // Process each pixel in the image
375    for i in 0..height {
376        for j in 0..width {
377            // Extract window around current pixel
378            let window_i_start = i;
379            let window_i_end = i + 2 * half_kernel + 1;
380            let window_j_start = j;
381            let window_j_end = j + 2 * half_kernel + 1;
382
383            // Ensure window is within bounds
384            if window_i_end > paddedimage.dim().0 || window_j_end > paddedimage.dim().1 {
385                return Err(SignalError::DimensionMismatch(
386                    "Window extends beyond padded image bounds".to_string(),
387                ));
388            }
389
390            // Extract window values
391            let window = paddedimage.slice(s![
392                window_i_start..window_i_end,
393                window_j_start..window_j_end
394            ]);
395
396            // Flatten and sort window values
397            let mut flat_window: Vec<f64> = window.iter().copied().collect();
398            flat_window.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
399
400            // Set output to median value
401            let median_idx = flat_window.len() / 2;
402            filtered[[i, j]] = flat_window[median_idx];
403        }
404    }
405
406    Ok(filtered)
407}
408
409/// Applies center-weighted median filtering to a 2D image
410#[allow(dead_code)]
411fn center_weighted_median_filter_2d(
412    image: &Array2<f64>,
413    paddedimage: &Array2<f64>,
414    half_kernel: usize,
415    config: &MedianConfig,
416) -> SignalResult<Array2<f64>> {
417    let (height, width) = image.dim();
418    let mut filtered = Array2::zeros((height, width));
419
420    // Calculate the center position in the _kernel
421    let center_i = half_kernel;
422    let center_j = half_kernel;
423
424    // Process each pixel in the image
425    for i in 0..height {
426        for j in 0..width {
427            // Extract window around current pixel
428            let window_i_start = i;
429            let window_i_end = i + 2 * half_kernel + 1;
430            let window_j_start = j;
431            let window_j_end = j + 2 * half_kernel + 1;
432
433            // Ensure window is within bounds
434            if window_i_end > paddedimage.dim().0 || window_j_end > paddedimage.dim().1 {
435                return Err(SignalError::DimensionMismatch(
436                    "Window extends beyond padded image bounds".to_string(),
437                ));
438            }
439
440            // Create weighted window with repeated center value
441            let mut weighted_window = Vec::new();
442
443            for wi in 0..(2 * half_kernel + 1) {
444                for wj in 0..(2 * half_kernel + 1) {
445                    let value = paddedimage[[window_i_start + wi, window_j_start + wj]];
446
447                    // Add center value with higher weight
448                    if wi == center_i && wj == center_j {
449                        for _ in 0..config.center_weight {
450                            weighted_window.push(value);
451                        }
452                    } else {
453                        weighted_window.push(value);
454                    }
455                }
456            }
457
458            // Sort the weighted window values
459            weighted_window.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
460
461            // Calculate the median position
462            let median_idx = weighted_window.len() / 2;
463
464            // Set output to weighted median value
465            filtered[[i, j]] = weighted_window[median_idx];
466        }
467    }
468
469    Ok(filtered)
470}
471
472/// Applies adaptive median filtering to a 2D image
473#[allow(dead_code)]
474fn adaptive_median_filter_2d(
475    image: &Array2<f64>,
476    paddedimage: &Array2<f64>,
477    initial_half_kernel: usize,
478    config: &MedianConfig,
479) -> SignalResult<Array2<f64>> {
480    let (height, width) = image.dim();
481    let mut filtered = Array2::zeros((height, width));
482
483    // Maximum half _kernel size
484    let max_half_kernel = config.max_kernel_size / 2;
485
486    // Process each pixel in the image
487    for i in 0..height {
488        for j in 0..width {
489            // Start with the initial _kernel size
490            let mut half_kernel = initial_half_kernel;
491
492            // Get current pixel value
493            let curr_val = paddedimage[[i + half_kernel, j + half_kernel]];
494
495            // Adaptive window size adjustment
496            while half_kernel <= max_half_kernel {
497                let kernel_size = 2 * half_kernel + 1;
498
499                // Calculate offset from initial to current window
500                let offset = half_kernel - initial_half_kernel;
501
502                // Extract window around current pixel
503                let window_i_start = i + offset;
504                let window_i_end = window_i_start + kernel_size;
505                let window_j_start = j + offset;
506                let window_j_end = window_j_start + kernel_size;
507
508                // Ensure window is within bounds
509                if window_i_end > paddedimage.dim().0 || window_j_end > paddedimage.dim().1 {
510                    break;
511                }
512
513                // Extract window
514                let window = paddedimage.slice(s![
515                    window_i_start..window_i_end,
516                    window_j_start..window_j_end
517                ]);
518
519                // Flatten and sort window values
520                let mut flat_window: Vec<f64> = window.iter().copied().collect();
521                flat_window.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
522
523                // Calculate window statistics
524                let min_val = flat_window[0];
525                let max_val = flat_window[flat_window.len() - 1];
526                let median_idx = flat_window.len() / 2;
527                let median = flat_window[median_idx];
528
529                // Level A: Test if median is impulse
530                let level_a = min_val < median && median < max_val;
531
532                if level_a {
533                    // Level B: Test if current pixel is impulse
534                    let level_b = min_val < curr_val && curr_val < max_val;
535
536                    if level_b {
537                        // Not an impulse, keep original value
538                        filtered[[i, j]] = curr_val;
539                    } else {
540                        // Impulse detected, use median
541                        filtered[[i, j]] = median;
542                    }
543
544                    // Exit the window size adjustment loop
545                    break;
546                } else {
547                    // Median might be impulse, increase window size
548                    half_kernel += 1;
549
550                    // If we've reached the maximum window size, use the median
551                    if half_kernel > max_half_kernel {
552                        filtered[[i, j]] = median;
553                    }
554                }
555            }
556        }
557    }
558
559    Ok(filtered)
560}
561
562/// Applies median filtering to a color image.
563///
564/// This function processes each color channel independently or jointly
565/// depending on the specified method.
566///
567/// # Arguments
568/// * `image` - Input color image (3D array with last axis being color channels)
569/// * `kernel_size` - Size of the filtering window (must be odd)
570/// * `config` - Filtering configuration
571/// * `vector_median` - Whether to use vector median filtering (preserves color relationships)
572///
573/// # Returns
574/// * The filtered color image
575#[allow(dead_code)]
576pub fn median_filter_color(
577    image: &Array3<f64>,
578    kernel_size: usize,
579    config: &MedianConfig,
580    vector_median: bool,
581) -> SignalResult<Array3<f64>> {
582    let (height, width, channels) = image.dim();
583
584    if vector_median {
585        // Vector _median filtering (preserves color relationships)
586        vector_median_filter(image, kernel_size, config)
587    } else {
588        // Channel-by-channel _median filtering
589        let mut filtered = Array3::zeros((height, width, channels));
590
591        for c in 0..channels {
592            // Extract channel
593            let channel = image.index_axis(Axis(2), c).to_owned();
594
595            // Apply _median filtering to the channel
596            let filtered_channel = median_filter_2d(&channel, kernel_size, config)?;
597
598            // Store result
599            for i in 0..height {
600                for j in 0..width {
601                    filtered[[i, j, c]] = filtered_channel[[i, j]];
602                }
603            }
604        }
605
606        Ok(filtered)
607    }
608}
609
610/// Applies vector median filtering to a color image.
611///
612/// Vector median filtering preserves color relationships by treating
613/// RGB pixels as vectors and finding the pixel with minimum sum of
614/// distances to other pixels in the window.
615///
616/// # Arguments
617/// * `image` - Input color image
618/// * `kernel_size` - Size of the filtering window
619/// * `config` - Filtering configuration
620///
621/// # Returns
622/// * The filtered color image
623#[allow(dead_code)]
624fn vector_median_filter(
625    image: &Array3<f64>,
626    kernel_size: usize,
627    config: &MedianConfig,
628) -> SignalResult<Array3<f64>> {
629    // Validate kernel _size
630    if kernel_size % 2 != 1 {
631        return Err(SignalError::ValueError(
632            "Kernel _size must be odd".to_string(),
633        ));
634    }
635
636    let (height, width, channels) = image.dim();
637
638    // If image is too small, return a copy
639    if height <= 1 || width <= 1 || kernel_size > height || kernel_size > width {
640        return Ok(image.clone());
641    }
642
643    let half_kernel = kernel_size / 2;
644
645    // Create padded image for each channel based on edge mode
646    let mut padded_channels = Vec::with_capacity(channels);
647    for c in 0..channels {
648        let channel = image.index_axis(Axis(2), c).to_owned();
649        padded_channels.push(padimage_2d(&channel, half_kernel, config.edge_mode));
650    }
651
652    // Allocate output image
653    let mut filtered = Array3::zeros((height, width, channels));
654
655    // Process each pixel in the image
656    for i in 0..height {
657        for j in 0..width {
658            // Extract windows around current pixel for each channel
659            let window_i_start = i;
660            let window_i_end = i + 2 * half_kernel + 1;
661            let window_j_start = j;
662            let window_j_end = j + 2 * half_kernel + 1;
663
664            // Ensure window is within bounds
665            if window_i_end > padded_channels[0].dim().0
666                || window_j_end > padded_channels[0].dim().1
667            {
668                return Err(SignalError::DimensionMismatch(
669                    "Window extends beyond padded image bounds".to_string(),
670                ));
671            }
672
673            // Extract all pixels in the window as vectors
674            let kernel_size = 2 * half_kernel + 1;
675            let window_size = kernel_size * kernel_size;
676            let mut window_vectors = Vec::with_capacity(window_size);
677
678            for wi in 0..kernel_size {
679                for wj in 0..kernel_size {
680                    let pi = window_i_start + wi;
681                    let pj = window_j_start + wj;
682
683                    // Extract color vector for this pixel
684                    let mut color_vector = Vec::with_capacity(channels);
685                    for (_c, padded_channel) in padded_channels.iter().enumerate().take(channels) {
686                        color_vector.push(padded_channel[[pi, pj]]);
687                    }
688
689                    window_vectors.push(color_vector);
690                }
691            }
692
693            // Find the vector median
694            let vector_median = find_vector_median(&window_vectors);
695
696            // Store the result
697            for (c, value) in vector_median.iter().enumerate().take(channels) {
698                filtered[[i, j, c]] = *value;
699            }
700        }
701    }
702
703    Ok(filtered)
704}
705
706/// Finds the vector median in a collection of vectors
707///
708/// The vector median is the vector that minimizes the sum of
709/// distances to all other vectors in the collection.
710#[allow(dead_code)]
711fn find_vector_median(vectors: &[Vec<f64>]) -> Vec<f64> {
712    if vectors.is_empty() {
713        return Vec::new();
714    }
715
716    if vectors.len() == 1 {
717        return vectors[0].clone();
718    }
719
720    // Calculate sum of distances for each vector
721    let mut min_distance_sum = f64::INFINITY;
722    let mut median_idx = 0;
723
724    for i in 0..vectors.len() {
725        let mut distance_sum = 0.0;
726
727        for j in 0..vectors.len() {
728            if i != j {
729                distance_sum += euclidean_distance(&vectors[i], &vectors[j]);
730            }
731        }
732
733        if distance_sum < min_distance_sum {
734            min_distance_sum = distance_sum;
735            median_idx = i;
736        }
737    }
738
739    vectors[median_idx].clone()
740}
741
742/// Computes the Euclidean distance between two vectors
743#[allow(dead_code)]
744fn euclidean_distance(v1: &[f64], v2: &[f64]) -> f64 {
745    if v1.len() != v2.len() {
746        return f64::INFINITY;
747    }
748
749    let mut sum_squared = 0.0;
750    for i in 0..v1.len() {
751        let diff = v1[i] - v2[i];
752        sum_squared += diff * diff;
753    }
754
755    sum_squared.sqrt()
756}
757
758/// Applies rank-order filtering to a 1D signal.
759///
760/// Rank-order filtering is a generalization of median filtering where any
761/// rank (percentile) can be selected instead of just the median (50th percentile).
762///
763/// # Arguments
764/// * `signal` - Input signal
765/// * `kernel_size` - Size of the filtering window
766/// * `rank` - Rank to select (0.0 = minimum, 0.5 = median, 1.0 = maximum)
767/// * `edge_mode` - Edge handling mode
768///
769/// # Returns
770/// * The filtered signal
771#[allow(dead_code)]
772pub fn rank_filter_1d(
773    signal: &Array1<f64>,
774    kernel_size: usize,
775    rank: f64,
776    edge_mode: EdgeMode,
777) -> SignalResult<Array1<f64>> {
778    // Validate parameters
779    if kernel_size % 2 != 1 {
780        return Err(SignalError::ValueError(
781            "Kernel _size must be odd".to_string(),
782        ));
783    }
784
785    if !(0.0..=1.0).contains(&rank) {
786        return Err(SignalError::ValueError(
787            "Rank must be between 0.0 and 1.0".to_string(),
788        ));
789    }
790
791    let n = signal.len();
792
793    // If signal is too short, return a copy
794    if n <= 1 || kernel_size > n {
795        return Ok(signal.clone());
796    }
797
798    let half_kernel = kernel_size / 2;
799
800    // Create padded signal based on edge _mode
801    let paddedsignal = padsignal_1d(signal, half_kernel, edge_mode);
802
803    // Apply rank filter
804    let mut filtered = Array1::zeros(n);
805
806    // Process each point in the signal
807    for i in 0..n {
808        // Extract window around current point
809        let window_start = i;
810        let window_end = i + 2 * half_kernel + 1;
811
812        // Ensure window is within bounds
813        if window_start >= paddedsignal.len() || window_end > paddedsignal.len() {
814            return Err(SignalError::DimensionMismatch(
815                "Window extends beyond padded signal bounds".to_string(),
816            ));
817        }
818
819        // Extract and sort window values
820        let mut window: Vec<f64> = paddedsignal.slice(s![window_start..window_end]).to_vec();
821        window.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
822
823        // Calculate the index for the requested rank
824        let rank_idx = ((window.len() - 1) as f64 * rank).round() as usize;
825
826        // Set output to the value at the specified rank
827        filtered[i] = window[rank_idx];
828    }
829
830    Ok(filtered)
831}
832
833/// Applies hybrid median filtering to a 2D image.
834///
835/// Hybrid median filtering uses multiple structural elements (like crosses and Xs)
836/// to better preserve edges in different orientations.
837///
838/// # Arguments
839/// * `image` - Input image
840/// * `kernel_size` - Size of the filtering window
841/// * `config` - Filtering configuration
842///
843/// # Returns
844/// * The filtered image
845#[allow(dead_code)]
846pub fn hybrid_median_filter_2d(
847    image: &Array2<f64>,
848    kernel_size: usize,
849    config: &MedianConfig,
850) -> SignalResult<Array2<f64>> {
851    // Validate kernel _size
852    if kernel_size % 2 != 1 {
853        return Err(SignalError::ValueError(
854            "Kernel _size must be odd".to_string(),
855        ));
856    }
857
858    let (height, width) = image.dim();
859
860    // If image is too small, return a copy
861    if height <= 1 || width <= 1 || kernel_size > height || kernel_size > width {
862        return Ok(image.clone());
863    }
864
865    let half_kernel = kernel_size / 2;
866
867    // Create padded image based on edge mode
868    let paddedimage = padimage_2d(image, half_kernel, config.edge_mode);
869
870    // Allocate output image
871    let mut filtered = Array2::zeros((height, width));
872
873    // Process each pixel in the image
874    for i in 0..height {
875        for j in 0..width {
876            // Extract window around current pixel
877            let window_i_start = i;
878            let window_i_end = i + 2 * half_kernel + 1;
879            let window_j_start = j;
880            let window_j_end = j + 2 * half_kernel + 1;
881
882            // Ensure window is within bounds
883            if window_i_end > paddedimage.dim().0 || window_j_end > paddedimage.dim().1 {
884                return Err(SignalError::DimensionMismatch(
885                    "Window extends beyond padded image bounds".to_string(),
886                ));
887            }
888
889            // Extract pixels from different structural elements
890            let mut plusshape = Vec::new(); // + shape
891            let mut crossshape = Vec::new(); // X shape
892
893            for k in 0..(2 * half_kernel + 1) {
894                // Horizontal line (part of + shape)
895                plusshape.push(paddedimage[[window_i_start + half_kernel, window_j_start + k]]);
896
897                // Vertical line (part of + shape)
898                plusshape.push(paddedimage[[window_i_start + k, window_j_start + half_kernel]]);
899
900                // Diagonal 1 (part of X shape)
901                if k < kernel_size {
902                    let diag_i = window_i_start + k;
903                    let diag_j = window_j_start + k;
904                    crossshape.push(paddedimage[[diag_i, diag_j]]);
905                }
906
907                // Diagonal 2 (part of X shape)
908                if k < kernel_size {
909                    let diag_i = window_i_start + k;
910                    let diag_j = window_j_start + kernel_size - 1 - k;
911                    crossshape.push(paddedimage[[diag_i, diag_j]]);
912                }
913            }
914
915            // Remove duplicate center pixel
916            if !plusshape.is_empty() {
917                plusshape.pop();
918            }
919
920            // Sort the values from each shape
921            plusshape.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
922            crossshape.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
923
924            // Get median for each shape
925            let plus_median = plusshape[plusshape.len() / 2];
926            let cross_median = crossshape[crossshape.len() / 2];
927
928            // Get the original pixel value
929            let orig_value =
930                paddedimage[[window_i_start + half_kernel, window_j_start + half_kernel]];
931
932            // Find the median of the three values: plus_median, cross_median, original
933            let mut final_values = [plus_median, cross_median, orig_value];
934            final_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
935
936            // Set output to the median of the three values
937            filtered[[i, j]] = final_values[1];
938        }
939    }
940
941    Ok(filtered)
942}
943
944/// Helper function to pad a 1D signal for edge handling
945#[allow(dead_code)]
946fn padsignal_1d(signal: &Array1<f64>, pad_size: usize, edgemode: EdgeMode) -> Array1<f64> {
947    let n = signal.len();
948    let mut padded = Array1::zeros(n + 2 * pad_size);
949
950    // Copy original signal
951    for i in 0..n {
952        padded[i + pad_size] = signal[i];
953    }
954
955    // Apply padding based on edge _mode
956    match edgemode {
957        EdgeMode::Reflect => {
958            // Reflect the signal at boundaries
959            for i in 0..pad_size {
960                // Left boundary: reflect
961                padded[pad_size - 1 - i] = signal[i.min(n - 1)];
962
963                // Right boundary: reflect
964                padded[n + pad_size + i] = signal[n - 1 - i.min(n - 1)];
965            }
966        }
967        EdgeMode::Nearest => {
968            // Pad with the nearest valid value
969            let first_val = signal[0];
970            let last_val = signal[n - 1];
971
972            for i in 0..pad_size {
973                padded[i] = first_val;
974                padded[n + pad_size + i] = last_val;
975            }
976        }
977        EdgeMode::Constant(value) => {
978            // Pad with a constant value
979            for i in 0..pad_size {
980                padded[i] = value;
981                padded[n + pad_size + i] = value;
982            }
983        }
984        EdgeMode::Wrap => {
985            // Wrap around (circular padding)
986            for i in 0..pad_size {
987                padded[i] = signal[(n - pad_size + i) % n];
988                padded[n + pad_size + i] = signal[i % n];
989            }
990        }
991    }
992
993    padded
994}
995
996/// Helper function to pad a 2D image for edge handling
997#[allow(dead_code)]
998fn padimage_2d(image: &Array2<f64>, pad_size: usize, edgemode: EdgeMode) -> Array2<f64> {
999    let (height, width) = image.dim();
1000    let mut padded = Array2::zeros((height + 2 * pad_size, width + 2 * pad_size));
1001
1002    // Copy original image
1003    for i in 0..height {
1004        for j in 0..width {
1005            padded[[i + pad_size, j + pad_size]] = image[[i, j]];
1006        }
1007    }
1008
1009    // Apply padding based on edge _mode
1010    match edgemode {
1011        EdgeMode::Reflect => {
1012            // Reflect the image at boundaries
1013
1014            // Top and bottom edges
1015            for i in 0..pad_size {
1016                for j in 0..width {
1017                    // Top edge
1018                    padded[[pad_size - 1 - i, j + pad_size]] = image[[i.min(height - 1), j]];
1019
1020                    // Bottom edge
1021                    padded[[height + pad_size + i, j + pad_size]] =
1022                        image[[height - 1 - i.min(height - 1), j]];
1023                }
1024            }
1025
1026            // Left and right edges
1027            for i in 0..height + 2 * pad_size {
1028                for j in 0..pad_size {
1029                    // Map to valid row in the padded image
1030                    let src_i = if i < pad_size {
1031                        2 * pad_size - i - 1
1032                    } else if i >= height + pad_size {
1033                        2 * (height + pad_size) - i - 1
1034                    } else {
1035                        i
1036                    };
1037
1038                    // Left edge
1039                    padded[[i, pad_size - 1 - j]] = padded[[src_i, pad_size + j.min(width - 1)]];
1040
1041                    // Right edge
1042                    padded[[i, width + pad_size + j]] =
1043                        padded[[src_i, width + pad_size - 1 - j.min(width - 1)]];
1044                }
1045            }
1046        }
1047        EdgeMode::Nearest => {
1048            // Pad with the nearest valid value
1049
1050            // Top and bottom edges
1051            for i in 0..pad_size {
1052                for j in 0..width {
1053                    // Top edge
1054                    padded[[i, j + pad_size]] = image[[0, j]];
1055
1056                    // Bottom edge
1057                    padded[[height + pad_size + i, j + pad_size]] = image[[height - 1, j]];
1058                }
1059            }
1060
1061            // Left and right edges
1062            for i in 0..height + 2 * pad_size {
1063                for j in 0..pad_size {
1064                    // Get the nearest valid column
1065                    let col_left = 0;
1066                    let col_right = width - 1;
1067
1068                    // Map to valid row
1069                    let row = if i < pad_size {
1070                        0
1071                    } else if i >= height + pad_size {
1072                        height - 1
1073                    } else {
1074                        i - pad_size
1075                    };
1076
1077                    // Left edge
1078                    padded[[i, j]] = image[[row, col_left]];
1079
1080                    // Right edge
1081                    padded[[i, width + pad_size + j]] = image[[row, col_right]];
1082                }
1083            }
1084        }
1085        EdgeMode::Constant(value) => {
1086            // Pad with a constant value
1087
1088            // Top and bottom edges
1089            for i in 0..pad_size {
1090                for j in 0..width + 2 * pad_size {
1091                    padded[[i, j]] = value;
1092                    padded[[height + pad_size + i, j]] = value;
1093                }
1094            }
1095
1096            // Left and right edges
1097            for i in pad_size..height + pad_size {
1098                for j in 0..pad_size {
1099                    padded[[i, j]] = value;
1100                    padded[[i, width + pad_size + j]] = value;
1101                }
1102            }
1103        }
1104        EdgeMode::Wrap => {
1105            // Wrap around (circular padding)
1106
1107            // Top and bottom edges
1108            for i in 0..pad_size {
1109                for j in 0..width {
1110                    // Top edge
1111                    padded[[i, j + pad_size]] = image[[(height - pad_size + i) % height, j]];
1112
1113                    // Bottom edge
1114                    padded[[height + pad_size + i, j + pad_size]] = image[[i % height, j]];
1115                }
1116            }
1117
1118            // Left and right edges
1119            for i in 0..height + 2 * pad_size {
1120                for j in 0..pad_size {
1121                    // Map to valid row in the padded image
1122                    let src_i = if i < pad_size {
1123                        (height - pad_size + i) % height + pad_size
1124                    } else if i >= height + pad_size {
1125                        (i - pad_size) % height + pad_size
1126                    } else {
1127                        i
1128                    };
1129
1130                    // Left edge
1131                    padded[[i, j]] = padded[[src_i, width + j]];
1132
1133                    // Right edge
1134                    padded[[i, width + pad_size + j]] = padded[[src_i, pad_size + j]];
1135                }
1136            }
1137        }
1138    }
1139
1140    padded
1141}