Skip to main content

resamplescope/
filters.rs

1use std::f64::consts::PI;
2
3#[derive(Debug, Clone, Copy, PartialEq)]
4pub enum KnownFilter {
5    Box,
6    Triangle,
7    Hermite,
8    CatmullRom,
9    Mitchell,
10    BSpline,
11    Lanczos2,
12    Lanczos3,
13    Lanczos4,
14    MitchellNetravali { b: f64, c: f64 },
15}
16
17impl KnownFilter {
18    pub fn name(&self) -> &'static str {
19        match self {
20            Self::Box => "Box",
21            Self::Triangle => "Triangle",
22            Self::Hermite => "Hermite",
23            Self::CatmullRom => "Catmull-Rom",
24            Self::Mitchell => "Mitchell",
25            Self::BSpline => "B-Spline",
26            Self::Lanczos2 => "Lanczos2",
27            Self::Lanczos3 => "Lanczos3",
28            Self::Lanczos4 => "Lanczos4",
29            Self::MitchellNetravali { .. } => "Mitchell-Netravali",
30        }
31    }
32
33    pub fn support(&self) -> f64 {
34        match self {
35            Self::Box => 0.5,
36            Self::Triangle | Self::Hermite => 1.0,
37            Self::CatmullRom | Self::Mitchell | Self::BSpline | Self::MitchellNetravali { .. } => {
38                2.0
39            }
40            Self::Lanczos2 => 2.0,
41            Self::Lanczos3 => 3.0,
42            Self::Lanczos4 => 4.0,
43        }
44    }
45
46    pub fn evaluate(&self, x: f64) -> f64 {
47        match self {
48            Self::Box => box_filter(x),
49            Self::Triangle => triangle(x),
50            Self::Hermite => hermite(x),
51            Self::CatmullRom => mitchell_netravali(x, 0.0, 0.5),
52            Self::Mitchell => mitchell_netravali(x, 1.0 / 3.0, 1.0 / 3.0),
53            Self::BSpline => mitchell_netravali(x, 1.0, 0.0),
54            Self::MitchellNetravali { b, c } => mitchell_netravali(x, *b, *c),
55            Self::Lanczos2 => lanczos(x, 2),
56            Self::Lanczos3 => lanczos(x, 3),
57            Self::Lanczos4 => lanczos(x, 4),
58        }
59    }
60
61    /// All built-in named filters for scoring.
62    pub fn all_named() -> &'static [KnownFilter] {
63        &[
64            KnownFilter::Box,
65            KnownFilter::Triangle,
66            KnownFilter::Hermite,
67            KnownFilter::CatmullRom,
68            KnownFilter::Mitchell,
69            KnownFilter::BSpline,
70            KnownFilter::Lanczos2,
71            KnownFilter::Lanczos3,
72            KnownFilter::Lanczos4,
73        ]
74    }
75}
76
77impl std::fmt::Display for KnownFilter {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            Self::MitchellNetravali { b, c } => write!(f, "Mitchell-Netravali(B={b:.3}, C={c:.3})"),
81            other => f.write_str(other.name()),
82        }
83    }
84}
85
86fn sinc(x: f64) -> f64 {
87    if x.abs() < 1e-10 {
88        1.0
89    } else {
90        let px = PI * x;
91        px.sin() / px
92    }
93}
94
95fn box_filter(x: f64) -> f64 {
96    let ax = x.abs();
97    if ax < 0.5 {
98        1.0
99    } else if (ax - 0.5).abs() < 1e-10 {
100        0.5
101    } else {
102        0.0
103    }
104}
105
106fn triangle(x: f64) -> f64 {
107    let ax = x.abs();
108    if ax < 1.0 { 1.0 - ax } else { 0.0 }
109}
110
111fn hermite(x: f64) -> f64 {
112    let ax = x.abs();
113    if ax < 1.0 {
114        (2.0 * ax - 3.0) * ax * ax + 1.0
115    } else {
116        0.0
117    }
118}
119
120fn mitchell_netravali(x: f64, b: f64, c: f64) -> f64 {
121    let ax = x.abs();
122    if ax < 1.0 {
123        ((12.0 - 9.0 * b - 6.0 * c) * ax * ax * ax
124            + (-18.0 + 12.0 * b + 6.0 * c) * ax * ax
125            + (6.0 - 2.0 * b))
126            / 6.0
127    } else if ax < 2.0 {
128        ((-b - 6.0 * c) * ax * ax * ax
129            + (6.0 * b + 30.0 * c) * ax * ax
130            + (-12.0 * b - 48.0 * c) * ax
131            + (8.0 * b + 24.0 * c))
132            / 6.0
133    } else {
134        0.0
135    }
136}
137
138fn lanczos(x: f64, n: u32) -> f64 {
139    let ax = x.abs();
140    if ax < n as f64 {
141        sinc(x) * sinc(x / n as f64)
142    } else {
143        0.0
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn filter_values_at_zero() {
153        // Interpolating filters have f(0) = 1.
154        for f in &[
155            KnownFilter::Box,
156            KnownFilter::Triangle,
157            KnownFilter::Hermite,
158            KnownFilter::CatmullRom,
159            KnownFilter::Lanczos2,
160            KnownFilter::Lanczos3,
161            KnownFilter::Lanczos4,
162        ] {
163            let v = f.evaluate(0.0);
164            assert!((v - 1.0).abs() < 1e-10, "{}: f(0) = {v}", f.name());
165        }
166        // Non-interpolating (approximating) filters have f(0) < 1.
167        let m = KnownFilter::Mitchell.evaluate(0.0);
168        assert!((m - 8.0 / 9.0).abs() < 1e-10, "Mitchell f(0) = {m}");
169        let bs = KnownFilter::BSpline.evaluate(0.0);
170        assert!((bs - 2.0 / 3.0).abs() < 1e-10, "B-Spline f(0) = {bs}");
171    }
172
173    #[test]
174    fn filters_zero_outside_support() {
175        for f in KnownFilter::all_named() {
176            let s = f.support();
177            assert!(
178                f.evaluate(s + 0.5).abs() < 1e-10,
179                "{}: f({}) = {}",
180                f.name(),
181                s + 0.5,
182                f.evaluate(s + 0.5)
183            );
184        }
185    }
186
187    #[test]
188    fn triangle_at_known_points() {
189        assert!((KnownFilter::Triangle.evaluate(0.0) - 1.0).abs() < 1e-10);
190        assert!((KnownFilter::Triangle.evaluate(0.5) - 0.5).abs() < 1e-10);
191        assert!((KnownFilter::Triangle.evaluate(1.0)).abs() < 1e-10);
192    }
193
194    #[test]
195    fn catmull_rom_interpolating() {
196        // CatmullRom passes through data points: f(0)=1, f(1)=0
197        assert!((KnownFilter::CatmullRom.evaluate(0.0) - 1.0).abs() < 1e-10);
198        assert!((KnownFilter::CatmullRom.evaluate(1.0)).abs() < 1e-10);
199        assert!((KnownFilter::CatmullRom.evaluate(2.0)).abs() < 1e-10);
200    }
201
202    #[test]
203    fn lanczos_symmetry() {
204        for &x in &[0.3, 0.7, 1.5, 2.5] {
205            let pos = KnownFilter::Lanczos3.evaluate(x);
206            let neg = KnownFilter::Lanczos3.evaluate(-x);
207            assert!(
208                (pos - neg).abs() < 1e-10,
209                "Lanczos3 not symmetric at {x}: {pos} vs {neg}"
210            );
211        }
212    }
213
214    #[test]
215    fn bspline_partition_of_unity() {
216        // B-spline should sum to 1 across integer shifts
217        let x = 0.3;
218        let sum: f64 = (-3..=3)
219            .map(|k| KnownFilter::BSpline.evaluate(x - k as f64))
220            .sum();
221        assert!(
222            (sum - 1.0).abs() < 1e-10,
223            "B-spline partition of unity: sum = {sum}"
224        );
225    }
226}