scirs2-vision 0.5.0

Computer vision module for SciRS2 (scirs2-vision)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
//! Interpolation methods for image resampling
//!
//! This module provides various interpolation methods for image
//! resampling, including nearest-neighbor, bilinear, bicubic,
//! Lanczos, and edge-preserving interpolation.
//!
//! The edge-preserving interpolation method is particularly useful for:
//!
//! 1. Maintaining sharp edges while reducing noise
//! 2. Preserving detailed structures during upscaling
//! 3. Reducing artifacts in images with text or line art
//! 4. Improving quality of resized natural images with distinct boundaries

use crate::error::Result;
use image::{DynamicImage, GenericImageView, ImageBuffer, Pixel, Rgba};
use scirs2_core::ndarray::{Array1, Array2};

/// Interpolation methods for image resampling
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum InterpolationMethod {
    /// Nearest-neighbor interpolation (fast but low quality)
    Nearest,
    /// Bilinear interpolation (good balance of speed and quality)
    #[default]
    Bilinear,
    /// Bicubic interpolation (higher quality)
    Bicubic,
    /// Lanczos interpolation (best quality)
    Lanczos3,
    /// Edge-preserving interpolation (preserves edges while smoothing)
    EdgePreserving,
}

/// Resize an image using the specified interpolation method
///
/// # Arguments
///
/// * `src` - Source image
/// * `width` - New width
/// * `height` - New height
/// * `method` - Interpolation method
///
/// # Returns
///
/// * Result containing resized image
#[allow(dead_code)]
pub fn resize(
    src: &DynamicImage,
    width: u32,
    height: u32,
    method: InterpolationMethod,
) -> Result<DynamicImage> {
    // Get source dimensions
    let (src_width, src_height) = src.dimensions();

    // If dimensions are the same, just return a copy
    if src_width == width && src_height == height {
        return Ok(src.clone());
    }

    // Create output image
    let mut dst: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::new(width, height);

    // Scale factors
    let scale_x = src_width as f64 / width as f64;
    let scale_y = src_height as f64 / height as f64;

    match method {
        InterpolationMethod::Nearest => {
            // Nearest neighbor interpolation
            for y in 0..height {
                for x in 0..width {
                    // Calculate source coordinates
                    let src_x = (x as f64 * scale_x).floor() as u32;
                    let src_y = (y as f64 * scale_y).floor() as u32;

                    // Clamp to source dimensions
                    let src_x = src_x.min(src_width - 1);
                    let src_y = src_y.min(src_height - 1);

                    // Sample nearest pixel
                    let pixel = src.get_pixel(src_x, src_y);
                    dst.put_pixel(x, y, pixel);
                }
            }
        }
        InterpolationMethod::Bilinear => {
            // Bilinear interpolation
            for y in 0..height {
                for x in 0..width {
                    // Calculate source coordinates
                    let src_x = x as f64 * scale_x;
                    let src_y = y as f64 * scale_y;

                    // Get pixel value using bilinear interpolation
                    let pixel = bilinear_interpolate(src, src_x, src_y);
                    dst.put_pixel(x, y, pixel);
                }
            }
        }
        InterpolationMethod::Bicubic => {
            // Bicubic interpolation
            for y in 0..height {
                for x in 0..width {
                    // Calculate source coordinates
                    let src_x = x as f64 * scale_x;
                    let src_y = y as f64 * scale_y;

                    // Get pixel value using bicubic interpolation
                    let pixel = bicubic_interpolate(src, src_x, src_y);
                    dst.put_pixel(x, y, pixel);
                }
            }
        }
        InterpolationMethod::Lanczos3 => {
            // Lanczos interpolation (a=3)
            for y in 0..height {
                for x in 0..width {
                    // Calculate source coordinates
                    let src_x = x as f64 * scale_x;
                    let src_y = y as f64 * scale_y;

                    // Get pixel value using Lanczos interpolation
                    let pixel = lanczos_interpolate(src, src_x, src_y, 3);
                    dst.put_pixel(x, y, pixel);
                }
            }
        }
        InterpolationMethod::EdgePreserving => {
            // Edge-preserving interpolation
            return resize_edge_preserving(src, width, height);
        }
    }

    Ok(DynamicImage::ImageRgba8(dst))
}

/// Bilinear interpolation for a single pixel
///
/// # Arguments
///
/// * `src` - Source image
/// * `x` - X coordinate (fractional)
/// * `y` - Y coordinate (fractional)
///
/// # Returns
///
/// * Interpolated pixel value
#[allow(dead_code)]
fn bilinear_interpolate(src: &DynamicImage, x: f64, y: f64) -> Rgba<u8> {
    let (width, height) = src.dimensions();

    // Get integer and fractional components
    let x0 = x.floor() as u32;
    let y0 = y.floor() as u32;
    let x1 = (x0 + 1).min(width - 1);
    let y1 = (y0 + 1).min(height - 1);

    let dx = x - x0 as f64;
    let dy = y - y0 as f64;

    // Get the four surrounding pixels
    let p00 = src.get_pixel(x0, y0).to_rgba();
    let p01 = src.get_pixel(x0, y1).to_rgba();
    let p10 = src.get_pixel(x1, y0).to_rgba();
    let p11 = src.get_pixel(x1, y1).to_rgba();

    // Interpolate each channel separately
    let mut result = [0u8; 4];
    for c in 0..4 {
        // Bilinear interpolation formula
        let c00 = p00[c] as f64;
        let c01 = p01[c] as f64;
        let c10 = p10[c] as f64;
        let c11 = p11[c] as f64;

        let value = (1.0 - dx) * (1.0 - dy) * c00
            + dx * (1.0 - dy) * c10
            + (1.0 - dx) * dy * c01
            + dx * dy * c11;

        // Clamp to valid range
        result[c] = value.round().clamp(0.0, 255.0) as u8;
    }

    Rgba(result)
}

/// Cubic Hermite spline
#[allow(dead_code)]
fn cubic_hermite(x: f64) -> f64 {
    // Cubic Hermite spline kernel
    let x = x.abs();
    if x < 1.0 {
        return (2.0 - x * x * (3.0 - 2.0 * x)).clamp(0.0, 1.0);
    } else if x < 2.0 {
        return (4.0 - 8.0 * x + 5.0 * x * x - x * x * x).clamp(0.0, 1.0) / 2.0;
    }
    0.0
}

/// Bicubic interpolation for a single pixel
///
/// # Arguments
///
/// * `src` - Source image
/// * `x` - X coordinate (fractional)
/// * `y` - Y coordinate (fractional)
///
/// # Returns
///
/// * Interpolated pixel value
#[allow(dead_code)]
fn bicubic_interpolate(src: &DynamicImage, x: f64, y: f64) -> Rgba<u8> {
    let (width, height) = src.dimensions();

    // Base coordinates
    let x_base = x.floor() as i32;
    let y_base = y.floor() as i32;

    // Calculate weights
    let dx = x - x_base as f64;
    let dy = y - y_base as f64;

    // Pre-calculate cubic weights
    let wx = [
        cubic_hermite(dx + 1.0),
        cubic_hermite(dx),
        cubic_hermite(1.0 - dx),
        cubic_hermite(2.0 - dx),
    ];

    let wy = [
        cubic_hermite(dy + 1.0),
        cubic_hermite(dy),
        cubic_hermite(1.0 - dy),
        cubic_hermite(2.0 - dy),
    ];

    // Sample 4x4 grid around the point
    let mut result = [0.0; 4];

    for c in 0..4 {
        let mut sum = 0.0;
        let mut weight_sum = 0.0;

        for ky in 0..4 {
            let y_sample = y_base - 1 + ky;

            // Skip outside pixels
            if y_sample < 0 || y_sample >= height as i32 {
                continue;
            }

            for kx in 0..4 {
                let x_sample = x_base - 1 + kx;

                // Skip outside pixels
                if x_sample < 0 || x_sample >= width as i32 {
                    continue;
                }

                // Calculate weight
                let weight = wx[kx as usize] * wy[ky as usize];
                if weight > 0.0 {
                    // Get pixel value
                    let pixel = src.get_pixel(x_sample as u32, y_sample as u32).to_rgba();
                    sum += weight * pixel[c] as f64;
                    weight_sum += weight;
                }
            }
        }

        // Normalize and clamp
        if weight_sum > 0.0 {
            result[c] = (sum / weight_sum).round();
            result[c] = result[c].clamp(0.0, 255.0);
        }
    }

    Rgba([
        result[0] as u8,
        result[1] as u8,
        result[2] as u8,
        result[3] as u8,
    ])
}

/// Lanczos kernel function
#[allow(dead_code)]
fn lanczos(x: f64, a: i32) -> f64 {
    if x.abs() < f64::EPSILON {
        return 1.0;
    }
    if x.abs() >= a as f64 {
        return 0.0;
    }

    let a_f64 = a as f64;
    let pi_x = std::f64::consts::PI * x;
    (a_f64 * (pi_x / a_f64).sin() * (pi_x).sin()) / (pi_x * pi_x)
}

/// Lanczos interpolation for a single pixel
///
/// # Arguments
///
/// * `src` - Source image
/// * `x` - X coordinate (fractional)
/// * `y` - Y coordinate (fractional)
/// * `a` - Size of the Lanczos kernel (typically 2 or 3)
///
/// # Returns
///
/// * Interpolated pixel value
#[allow(dead_code)]
fn lanczos_interpolate(src: &DynamicImage, x: f64, y: f64, a: i32) -> Rgba<u8> {
    let (width, height) = src.dimensions();

    // Base coordinates
    let x_base = x.floor() as i32;
    let y_base = y.floor() as i32;

    // Calculate fractional parts
    let dx = x - x_base as f64;
    let dy = y - y_base as f64;

    // Setup kernel size
    let kernel_width = 2 * a;
    let mut weights_x = vec![0.0; kernel_width as usize];
    let mut weights_y = vec![0.0; kernel_width as usize];

    // Compute weights for x direction
    for k in 0..kernel_width {
        let kx = k - a + 1;
        weights_x[k as usize] = lanczos(dx - kx as f64, a);
    }

    // Compute weights for y direction
    for k in 0..kernel_width {
        let ky = k - a + 1;
        weights_y[k as usize] = lanczos(dy - ky as f64, a);
    }

    // Normalize weights
    let sum_wx: f64 = weights_x.iter().sum();
    let sum_wy: f64 = weights_y.iter().sum();

    for k in 0..kernel_width as usize {
        weights_x[k] /= sum_wx;
        weights_y[k] /= sum_wy;
    }

    // Compute interpolated color
    let mut result = [0.0; 4];

    for c in 0..4 {
        let mut sum = 0.0;
        let mut weight_sum = 0.0;

        for ky in 0..kernel_width {
            let y_sample = y_base + ky - a + 1;

            // Skip outside pixels
            if y_sample < 0 || y_sample >= height as i32 {
                continue;
            }

            for kx in 0..kernel_width {
                let x_sample = x_base + kx - a + 1;

                // Skip outside pixels
                if x_sample < 0 || x_sample >= width as i32 {
                    continue;
                }

                // Calculate separable kernel weight
                let weight = weights_x[kx as usize] * weights_y[ky as usize];

                // Add weighted contribution
                let pixel = src.get_pixel(x_sample as u32, y_sample as u32).to_rgba();
                sum += weight * pixel[c] as f64;
                weight_sum += weight;
            }
        }

        // Normalize and clamp
        if weight_sum > 0.0 {
            result[c] = (sum / weight_sum).round();
            result[c] = result[c].clamp(0.0, 255.0);
        }
    }

    Rgba([
        result[0] as u8,
        result[1] as u8,
        result[2] as u8,
        result[3] as u8,
    ])
}

/// Helper function to create 1D kernels for separable convolution
///
/// # Arguments
///
/// * `kernel_size` - Size of the kernel
///
/// # Returns
///
/// * Kernel as a 1D array
#[allow(dead_code)]
fn create_kernel(_kernelfunc: fn(f64) -> f64, kernel_size: usize, scale: f64) -> Array1<f64> {
    let mut kernel = Array1::zeros(kernel_size);
    let radius = (kernel_size as f64 - 1.0) / 2.0;

    for i in 0..kernel_size {
        let x = (i as f64 - radius) / scale;
        kernel[i] = _kernelfunc(x);
    }

    // Normalize kernel
    let sum = kernel.sum();
    if sum > 0.0 {
        kernel.mapv_inplace(|x| x / sum);
    }

    kernel
}

/// Calculate convolution of a row or column with a 1D kernel
///
/// # Arguments
///
/// * `src` - Image to convolve
/// * `kernel` - 1D convolution kernel
/// * `horizontal` - If true, convolve along rows (x), otherwise along columns (y)
///
/// # Returns
///
/// * Convolved image
#[allow(dead_code)]
pub fn convolve_1d(
    src: &Array2<f64>,
    kernel: &Array1<f64>,
    horizontal: bool,
) -> Result<Array2<f64>> {
    let (height, width) = src.dim();
    let mut dst = Array2::zeros((height, width));

    let k_size = kernel.len();
    let k_radius = (k_size / 2) as isize;

    if horizontal {
        // Convolve rows (horizontally)
        for y in 0..height {
            for x in 0..width {
                let mut sum = 0.0;
                let mut weight_sum = 0.0;

                for k in 0..k_size {
                    let kx = x as isize + (k as isize - k_radius);

                    // Handle boundary
                    if kx >= 0 && kx < width as isize {
                        let w = kernel[k];
                        sum += w * src[[y, kx as usize]];
                        weight_sum += w;
                    }
                }

                // Normalize
                if weight_sum > 0.0 {
                    dst[[y, x]] = sum / weight_sum;
                }
            }
        }
    } else {
        // Convolve columns (vertically)
        for y in 0..height {
            for x in 0..width {
                let mut sum = 0.0;
                let mut weight_sum = 0.0;

                for k in 0..k_size {
                    let ky = y as isize + (k as isize - k_radius);

                    // Handle boundary
                    if ky >= 0 && ky < height as isize {
                        let w = kernel[k];
                        sum += w * src[[ky as usize, x]];
                        weight_sum += w;
                    }
                }

                // Normalize
                if weight_sum > 0.0 {
                    dst[[y, x]] = sum / weight_sum;
                }
            }
        }
    }

    Ok(dst)
}

/// Resize image using separable convolution method
///
/// This provides high-quality resizing by first convolving the image
/// with an appropriate filter kernel and then sampling at the desired locations.
///
/// # Arguments
///
/// * `src` - Source image
/// * `width` - Target width
/// * `height` - Target height
/// * `kernel_func` - Function implementing the filter kernel
/// * `kernel_size` - Size of the kernel
///
/// # Returns
///
/// * Resized image
#[allow(dead_code)]
pub fn resize_convolution(
    src: &DynamicImage,
    width: u32,
    height: u32,
    kernel_func: fn(f64) -> f64,
    kernel_size: usize,
) -> Result<DynamicImage> {
    let (src_width, src_height) = src.dimensions();

    // If dimensions are the same, just return a copy
    if src_width == width && src_height == height {
        return Ok(src.clone());
    }

    // Calculate scale factors
    let scale_x = width as f64 / src_width as f64;
    let scale_y = height as f64 / src_height as f64;

    // Create destination image
    let mut dst: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::new(width, height);

    // Process each channel separately
    for c in 0..4 {
        // Extract channel
        let mut channel = Array2::zeros((src_height as usize, src_width as usize));
        for y in 0..src_height {
            for x in 0..src_width {
                let pixel = src.get_pixel(x, y).to_rgba();
                channel[[y as usize, x as usize]] = pixel[c] as f64;
            }
        }

        // Create kernels for horizontal and vertical convolution
        let scale_factor_x = if scale_x < 1.0 { scale_x } else { 1.0 };
        let scale_factor_y = if scale_y < 1.0 { scale_y } else { 1.0 };

        let kernel_x = create_kernel(kernel_func, kernel_size, scale_factor_x);
        let kernel_y = create_kernel(kernel_func, kernel_size, scale_factor_y);

        // Apply horizontal convolution
        let temp = convolve_1d(&channel, &kernel_x, true)?;

        // Apply vertical convolution
        let filtered = convolve_1d(&temp, &kernel_y, false)?;

        // Sample the filtered image at target resolution
        for y in 0..height {
            for x in 0..width {
                // Calculate source coordinates
                let src_x = (x as f64 + 0.5) / scale_x - 0.5;
                let src_y = (y as f64 + 0.5) / scale_y - 0.5;

                // Convert to integer and fractional components
                let x0 = src_x.floor() as i32;
                let y0 = src_y.floor() as i32;
                let dx = src_x - x0 as f64;
                let dy = src_y - y0 as f64;

                // Calculate bilinear interpolation weights
                let w00 = (1.0 - dx) * (1.0 - dy);
                let w01 = (1.0 - dx) * dy;
                let w10 = dx * (1.0 - dy);
                let w11 = dx * dy;

                // Get pixel values (with boundary handling)
                let mut value = 0.0;
                let mut weight_sum = 0.0;

                // Check and accumulate all valid samples
                for ky in 0..2 {
                    let y_index = y0 + ky;
                    if y_index >= 0 && y_index < src_height as i32 {
                        for kx in 0..2 {
                            let x_index = x0 + kx;
                            if x_index >= 0 && x_index < src_width as i32 {
                                let w = match (kx, ky) {
                                    (0, 0) => w00,
                                    (0, 1) => w01,
                                    (1, 0) => w10,
                                    (1, 1) => w11,
                                    _ => 0.0,
                                };

                                if w > 0.0 {
                                    value += w * filtered[[y_index as usize, x_index as usize]];
                                    weight_sum += w;
                                }
                            }
                        }
                    }
                }

                // Normalize if needed
                if weight_sum > 0.0 {
                    value /= weight_sum;
                }

                // Update pixel in destination image
                let mut pixel = dst.get_pixel_mut(x, y).to_rgba();
                pixel[c] = value.round().clamp(0.0, 255.0) as u8;
                dst.put_pixel(x, y, pixel);
            }
        }
    }

    Ok(DynamicImage::ImageRgba8(dst))
}

// Lanczos filter kernel function
#[allow(dead_code)]
fn lanczos_kernel(x: f64) -> f64 {
    lanczos(x, 3)
}

// Bicubic filter kernel function
#[allow(dead_code)]
fn bicubic_kernel(x: f64) -> f64 {
    cubic_hermite(x.abs())
}

/// Resize image using high-quality Lanczos resampling
///
/// # Arguments
///
/// * `src` - Source image
/// * `width` - Target width
/// * `height` - Target height
///
/// # Returns
///
/// * Resized image
#[allow(dead_code)]
pub fn resize_lanczos(src: &DynamicImage, width: u32, height: u32) -> Result<DynamicImage> {
    resize_convolution(src, width, height, lanczos_kernel, 7)
}

/// Resize image using high-quality bicubic resampling
///
/// # Arguments
///
/// * `src` - Source image
/// * `width` - Target width
/// * `height` - Target height
///
/// # Returns
///
/// * Resized image
#[allow(dead_code)]
pub fn resize_bicubic(src: &DynamicImage, width: u32, height: u32) -> Result<DynamicImage> {
    resize_convolution(src, width, height, bicubic_kernel, 5)
}

/// Guided filter for edge-preserving smoothing
///
/// # Arguments
///
/// * `guide` - Guide image (typically the input image itself)
/// * `src` - Source image to be filtered
/// * `radius` - Filter radius
/// * `epsilon` - Regularization parameter
///
/// # Returns
///
/// * Filtered image
#[allow(dead_code)]
fn guided_filter(
    guide: &Array2<f64>,
    src: &Array2<f64>,
    radius: usize,
    epsilon: f64,
) -> Result<Array2<f64>> {
    let (height, width) = guide.dim();

    // These variables will be filled later
    // Mean of guide image (I) in each local window
    let mean_i;
    // Mean of source image (p) in each local window
    let mean_p;
    // Mean of I*I in each local window
    let mean_ii;
    // Mean of I*p in each local window
    let mean_ip;

    // Create box filter kernel
    let box_kernel = Array1::from_elem(2 * radius + 1, 1.0 / ((2 * radius + 1) as f64));

    // Compute local means using box filter
    let mut temp_i = guide.clone();
    let mut temp_p = src.clone();

    // Compute horizontal convolution
    temp_i = convolve_1d(&temp_i, &box_kernel, true)?;
    temp_p = convolve_1d(&temp_p, &box_kernel, true)?;

    // Compute vertical convolution
    mean_i = convolve_1d(&temp_i, &box_kernel, false)?;
    mean_p = convolve_1d(&temp_p, &box_kernel, false)?;

    // Compute mean of I*I and I*p
    let mut ii = Array2::zeros((height, width));
    let mut ip = Array2::zeros((height, width));

    for y in 0..height {
        for x in 0..width {
            ii[[y, x]] = guide[[y, x]] * guide[[y, x]];
            ip[[y, x]] = guide[[y, x]] * src[[y, x]];
        }
    }

    // Apply box filter to ii and ip
    let mut temp_ii = ii.clone();
    let mut temp_ip = ip.clone();

    // Compute horizontal convolution
    temp_ii = convolve_1d(&temp_ii, &box_kernel, true)?;
    temp_ip = convolve_1d(&temp_ip, &box_kernel, true)?;

    // Compute vertical convolution
    mean_ii = convolve_1d(&temp_ii, &box_kernel, false)?;
    mean_ip = convolve_1d(&temp_ip, &box_kernel, false)?;

    // Compute covariance of (I, p) in each local patch
    let mut cov_ip = Array2::zeros((height, width));
    for y in 0..height {
        for x in 0..width {
            cov_ip[[y, x]] = mean_ip[[y, x]] - mean_i[[y, x]] * mean_p[[y, x]];
        }
    }

    // Compute variance of I in each local patch
    let mut var_i = Array2::zeros((height, width));
    for y in 0..height {
        for x in 0..width {
            var_i[[y, x]] = mean_ii[[y, x]] - mean_i[[y, x]] * mean_i[[y, x]];
        }
    }

    // Compute a and b for the linear model
    let mut a = Array2::zeros((height, width));
    let mut b = Array2::zeros((height, width));

    for y in 0..height {
        for x in 0..width {
            a[[y, x]] = cov_ip[[y, x]] / (var_i[[y, x]] + epsilon);
            b[[y, x]] = mean_p[[y, x]] - a[[y, x]] * mean_i[[y, x]];
        }
    }

    // Apply box filter to a and b
    // Compute horizontal convolution
    let temp_a = convolve_1d(&a, &box_kernel, true)?;
    let temp_b = convolve_1d(&b, &box_kernel, true)?;

    // Compute vertical convolution
    let mean_a = convolve_1d(&temp_a, &box_kernel, false)?;
    let mean_b = convolve_1d(&temp_b, &box_kernel, false)?;

    // Compute the output
    let mut output = Array2::zeros((height, width));
    for y in 0..height {
        for x in 0..width {
            output[[y, x]] = mean_a[[y, x]] * guide[[y, x]] + mean_b[[y, x]];
        }
    }

    Ok(output)
}

/// Resize image using edge-preserving interpolation
///
/// This method first resizes the image using Lanczos interpolation,
/// then applies a guided filter to preserve edges and reduce artifacts.
///
/// # Arguments
///
/// * `src` - Source image
/// * `width` - Target width
/// * `height` - Target height
///
/// # Returns
///
/// * Edge-preserving resized image
#[allow(dead_code)]
pub fn resize_edge_preserving(src: &DynamicImage, width: u32, height: u32) -> Result<DynamicImage> {
    // First resize using high-quality Lanczos
    let initial_resize = resize_lanczos(src, width, height)?;

    // Set guided filter parameters
    let radius = 2;
    let epsilon = 0.01;

    // Convert image to grayscale for guide
    let grayscale = initial_resize.to_luma8();

    // Create output image
    let mut dst: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::new(width, height);

    // Process each channel with guided filter
    for c in 0..3 {
        // Skip alpha channel
        // Extract channel from initial resize
        let mut channel = Array2::zeros((height as usize, width as usize));
        let mut guide = Array2::zeros((height as usize, width as usize));

        for y in 0..height {
            for x in 0..width {
                let pixel = initial_resize.get_pixel(x, y).to_rgba();
                channel[[y as usize, x as usize]] = pixel[c] as f64;

                // Use grayscale as guide
                let guide_value = grayscale.get_pixel(x, y)[0];
                guide[[y as usize, x as usize]] = guide_value as f64;
            }
        }

        // Apply guided filter
        let filtered = guided_filter(&guide, &channel, radius, epsilon)?;

        // Update output image
        for y in 0..height {
            for x in 0..width {
                let mut pixel = dst.get_pixel_mut(x, y).to_rgba();
                pixel[c] = filtered[[y as usize, x as usize]].round().clamp(0.0, 255.0) as u8;
                dst.put_pixel(x, y, pixel);
            }
        }
    }

    // Copy alpha channel directly
    for y in 0..height {
        for x in 0..width {
            let alpha = initial_resize.get_pixel(x, y).to_rgba()[3];
            let mut pixel = dst.get_pixel_mut(x, y).to_rgba();
            pixel[3] = alpha;
            dst.put_pixel(x, y, pixel);
        }
    }

    Ok(DynamicImage::ImageRgba8(dst))
}

#[cfg(test)]
mod tests {
    use super::*;
    use image::{DynamicImage, ImageBuffer, Rgba};

    #[test]
    fn test_lanczos_kernel() {
        // Lanczos kernel should be 1.0 at x=0
        assert!((lanczos_kernel(0.0) - 1.0).abs() < 1e-10);

        // Lanczos kernel should be 0.0 at x=3 (for a=3)
        assert!(lanczos_kernel(3.0).abs() < 1e-10);

        // Lanczos kernel should be symmetric
        for x in [0.5, 1.0, 1.5, 2.0, 2.5] {
            assert!((lanczos_kernel(x) - lanczos_kernel(-x)).abs() < 1e-10);
        }
    }

    #[test]
    fn test_bicubic_kernel() {
        // Bicubic kernel should be 1.0 at x=0
        assert!(bicubic_kernel(0.0) - 1.0 < 1e-10);

        // Bicubic kernel should be 0.0 at x=2 or beyond
        assert!(bicubic_kernel(2.0).abs() < 1e-10);
        assert!(bicubic_kernel(3.0).abs() < 1e-10);

        // Bicubic kernel should be symmetric
        for x in [0.5, 1.0, 1.5, 1.9] {
            assert!((bicubic_kernel(x) - bicubic_kernel(-x)).abs() < 1e-10);
        }
    }

    #[test]
    fn test_guided_filter() {
        // Create a simple test input
        let width = 10;
        let height = 10;
        let mut guide = Array2::zeros((height, width));
        let mut src = Array2::zeros((height, width));

        // Setup test data with an edge
        for y in 0..height {
            for x in 0..width {
                // Create a vertical edge in the middle
                if x < width / 2 {
                    guide[[y, x]] = 50.0;
                    src[[y, x]] = 50.0;
                } else {
                    guide[[y, x]] = 150.0;
                    src[[y, x]] = 150.0;
                }

                // Add some noise to the source
                if x % 2 == 0 && y % 2 == 0 {
                    src[[y, x]] += 20.0;
                }
            }
        }

        // Apply guided filter
        let radius = 1;
        let epsilon = 0.1;
        let result = guided_filter(&guide, &src, radius, epsilon).expect("Operation failed");

        // The filter should denoise but preserve the edge
        // Check that the edge is preserved
        let edge_preserved = result[[5, 4]] < 100.0 && result[[5, 5]] > 100.0;
        assert!(edge_preserved);

        // Check that noise is reduced
        let noise_reduced = (result[[2, 2]] - result[[2, 0]]).abs() < 10.0;
        assert!(noise_reduced);
    }

    #[test]
    fn test_resize_edge_preserving() {
        // Create a simple test image
        let width = 20;
        let height = 20;
        let mut img = ImageBuffer::new(width, height);

        // Create a pattern with edges
        for y in 0..height {
            for x in 0..width {
                let pixel_value = if x < width / 2 { 50 } else { 200 };
                img.put_pixel(x, y, Rgba([pixel_value, pixel_value, pixel_value, 255]));
            }
        }

        let src = DynamicImage::ImageRgba8(img);

        // Resize to smaller dimensions
        let result = resize_edge_preserving(&src, 10, 10).expect("Operation failed");

        // The edge should be preserved after resizing
        let edge_before = result.get_pixel(4, 5)[0];
        let edge_after = result.get_pixel(5, 5)[0];
        assert!(edge_after - edge_before > 50);

        // Also test upscaling
        let result_up = resize_edge_preserving(&src, 30, 30).expect("Operation failed");

        // Dimensions should be correct
        assert_eq!(result_up.width(), 30);
        assert_eq!(result_up.height(), 30);

        // The edge should be preserved after upscaling
        let edge_before_up = result_up.get_pixel(14, 15)[0];
        let edge_after_up = result_up.get_pixel(15, 15)[0];
        assert!(edge_after_up - edge_before_up > 50);
    }
}