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 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 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 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 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 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}