Skip to main content

trueno_image/
resize.rs

1//! Image resize: bilinear and nearest-neighbor interpolation (NPP parity).
2
3use crate::error::ImageError;
4
5/// Interpolation method for resize.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Interpolation {
8    /// Nearest-neighbor (fastest, blocky).
9    Nearest,
10    /// Bilinear interpolation (smooth).
11    Bilinear,
12    /// Bicubic interpolation (sharper than bilinear, slower).
13    Bicubic,
14    /// Lanczos interpolation with a=3 (highest quality, slowest).
15    Lanczos,
16}
17
18/// Resize a grayscale image.
19///
20/// # Errors
21///
22/// Returns error if dimensions don't match or output size is zero.
23pub fn resize(
24    image: &[f32],
25    src_w: usize,
26    src_h: usize,
27    dst_w: usize,
28    dst_h: usize,
29    method: Interpolation,
30) -> Result<Vec<f32>, ImageError> {
31    if image.len() != src_w * src_h {
32        return Err(ImageError::BufferLengthMismatch {
33            expected: src_w * src_h,
34            got: image.len(),
35            width: src_w,
36            height: src_h,
37        });
38    }
39    if dst_w == 0 || dst_h == 0 {
40        return Err(ImageError::ZeroDimension {
41            width: dst_w,
42            height: dst_h,
43        });
44    }
45
46    let mut output = vec![0.0f32; dst_w * dst_h];
47
48    let scale_x = src_w as f32 / dst_w as f32;
49    let scale_y = src_h as f32 / dst_h as f32;
50
51    for dy in 0..dst_h {
52        for dx in 0..dst_w {
53            let sx = (dx as f32 + 0.5) * scale_x - 0.5;
54            let sy = (dy as f32 + 0.5) * scale_y - 0.5;
55
56            output[dy * dst_w + dx] = match method {
57                Interpolation::Nearest => {
58                    let ix = (sx + 0.5) as usize;
59                    let iy = (sy + 0.5) as usize;
60                    let ix = ix.min(src_w - 1);
61                    let iy = iy.min(src_h - 1);
62                    image[iy * src_w + ix]
63                }
64                Interpolation::Bilinear => bilinear_sample(image, src_w, src_h, sx, sy),
65                Interpolation::Bicubic => bicubic_sample(image, src_w, src_h, sx, sy),
66                Interpolation::Lanczos => lanczos_sample(image, src_w, src_h, sx, sy),
67            };
68        }
69    }
70
71    Ok(output)
72}
73
74/// Clamp index to [0, size-1].
75#[inline]
76fn clamp_idx(i: isize, size: usize) -> usize {
77    i.clamp(0, size as isize - 1) as usize
78}
79
80/// Bilinear interpolation at fractional coordinates.
81fn bilinear_sample(image: &[f32], w: usize, h: usize, x: f32, y: f32) -> f32 {
82    let x0 = (x.floor() as isize).max(0) as usize;
83    let y0 = (y.floor() as isize).max(0) as usize;
84    let x1 = (x0 + 1).min(w - 1);
85    let y1 = (y0 + 1).min(h - 1);
86
87    let fx = (x - x0 as f32).clamp(0.0, 1.0);
88    let fy = (y - y0 as f32).clamp(0.0, 1.0);
89
90    let p00 = image[y0 * w + x0];
91    let p10 = image[y0 * w + x1];
92    let p01 = image[y1 * w + x0];
93    let p11 = image[y1 * w + x1];
94
95    p00 * (1.0 - fx) * (1.0 - fy) + p10 * fx * (1.0 - fy) + p01 * (1.0 - fx) * fy + p11 * fx * fy
96}
97
98/// Cubic interpolation weight (Keys' convolution, a = -0.5).
99#[inline]
100fn cubic_weight(t: f32) -> f32 {
101    let t = t.abs();
102    if t <= 1.0 {
103        (1.5 * t - 2.5) * t * t + 1.0
104    } else if t < 2.0 {
105        ((-0.5 * t + 2.5) * t - 4.0) * t + 2.0
106    } else {
107        0.0
108    }
109}
110
111/// Bicubic interpolation at fractional coordinates (4×4 neighborhood).
112fn bicubic_sample(image: &[f32], w: usize, h: usize, x: f32, y: f32) -> f32 {
113    let ix = x.floor() as isize;
114    let iy = y.floor() as isize;
115    let fx = x - ix as f32;
116    let fy = y - iy as f32;
117
118    let mut sum = 0.0f64;
119    for j in -1..=2_isize {
120        let wy = cubic_weight(fy - j as f32) as f64;
121        let cy = clamp_idx(iy + j, h);
122        for i in -1..=2_isize {
123            let wx = cubic_weight(fx - i as f32) as f64;
124            let cx = clamp_idx(ix + i, w);
125            sum += wy * wx * f64::from(image[cy * w + cx]);
126        }
127    }
128    sum as f32
129}
130
131/// Lanczos kernel with a=3.
132#[inline]
133fn lanczos_weight(t: f32) -> f32 {
134    let t = t.abs();
135    if t < 1e-7 {
136        1.0
137    } else if t < 3.0 {
138        let pi_t = std::f32::consts::PI * t;
139        let pi_t_over_a = pi_t / 3.0;
140        (pi_t.sin() * pi_t_over_a.sin()) / (pi_t * pi_t_over_a)
141    } else {
142        0.0
143    }
144}
145
146/// Lanczos interpolation at fractional coordinates (6×6 neighborhood, a=3).
147fn lanczos_sample(image: &[f32], w: usize, h: usize, x: f32, y: f32) -> f32 {
148    let ix = x.floor() as isize;
149    let iy = y.floor() as isize;
150    let fx = x - ix as f32;
151    let fy = y - iy as f32;
152
153    let mut sum = 0.0f64;
154    let mut weight_sum = 0.0f64;
155    for j in -2..=3_isize {
156        let wy = lanczos_weight(fy - j as f32) as f64;
157        let cy = clamp_idx(iy + j, h);
158        for i in -2..=3_isize {
159            let wx = lanczos_weight(fx - i as f32) as f64;
160            let cx = clamp_idx(ix + i, w);
161            let w_total = wy * wx;
162            sum += w_total * f64::from(image[cy * w + cx]);
163            weight_sum += w_total;
164        }
165    }
166    if weight_sum.abs() > 1e-12 {
167        (sum / weight_sum) as f32
168    } else {
169        0.0
170    }
171}