1use crate::representation::RepresentationError;
19
20pub trait FrameInterpolator: Send {
26 fn between(
29 &mut self,
30 a: &[f32],
31 b: &[f32],
32 width: usize,
33 height: usize,
34 fractions: &[f32],
35 ) -> Result<Vec<Vec<f32>>, InterpError>;
36}
37
38#[derive(Debug)]
39pub enum InterpError {
40 Unsupported(String),
42 Model(String),
44}
45
46impl std::fmt::Display for InterpError {
47 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Unsupported(message) | Self::Model(message) => formatter.write_str(message),
50 }
51 }
52}
53
54impl std::error::Error for InterpError {}
55
56impl From<InterpError> for RepresentationError {
57 fn from(error: InterpError) -> Self {
58 Self::Device(error.to_string())
59 }
60}
61
62pub struct LinearInterpolator;
68
69impl FrameInterpolator for LinearInterpolator {
70 fn between(
71 &mut self,
72 a: &[f32],
73 b: &[f32],
74 _width: usize,
75 _height: usize,
76 fractions: &[f32],
77 ) -> Result<Vec<Vec<f32>>, InterpError> {
78 Ok(fractions
79 .iter()
80 .map(|fraction| {
81 a.iter()
82 .zip(b)
83 .map(|(a, b)| a + (b - a) * fraction)
84 .collect()
85 })
86 .collect())
87 }
88}
89
90#[cfg(feature = "onnx")]
95#[derive(Clone, Debug, PartialEq, Eq)]
96enum Layout {
97 Stacked { image: String, timestep: Option<String> },
99 Separate {
101 first: String,
102 second: String,
103 timestep: Option<String>,
104 },
105}
106
107#[cfg(feature = "onnx")]
117pub struct OnnxInterpolator {
118 model: crate::model::Model,
119 layout: Layout,
120}
121
122#[cfg(feature = "onnx")]
123impl OnnxInterpolator {
124 pub fn load(path: &str) -> Result<Self, InterpError> {
126 let model = crate::model::Model::load(path).map_err(|error| InterpError::Model(error.to_string()))?;
127 let layout = Self::detect(&model)?;
128 Ok(Self { model, layout })
129 }
130
131 fn detect(model: &crate::model::Model) -> Result<Layout, InterpError> {
133 let images: Vec<&crate::model::Port> = model
135 .inputs()
136 .iter()
137 .filter(|port| port.shape.len() >= 3)
138 .collect();
139 let timestep = model
140 .inputs()
141 .iter()
142 .find(|port| port.shape.len() < 3 || port.shape.iter().all(|dim| *dim == 1))
143 .map(|port| port.name.clone());
144 let channels = |port: &crate::model::Port| port.shape.get(port.shape.len() - 3).copied();
145
146 match images.as_slice() {
147 [image] if matches!(channels(image), Some(6) | Some(-1)) => Ok(Layout::Stacked {
148 image: image.name.clone(),
149 timestep,
150 }),
151 [first, second] => Ok(Layout::Separate {
152 first: first.name.clone(),
153 second: second.name.clone(),
154 timestep,
155 }),
156 other => Err(InterpError::Unsupported(format!(
157 "a frame interpolator should take a frame pair — one six-channel input or two \
158 three-channel ones, optionally with a timestep — but this graph declares {} \
159 image-shaped inputs: {:?}",
160 other.len(),
161 model
162 .inputs()
163 .iter()
164 .map(|port| (port.name.as_str(), &port.shape))
165 .collect::<Vec<_>>()
166 ))),
167 }
168 }
169
170 fn timestep(&self) -> Option<&str> {
171 match &self.layout {
172 Layout::Stacked { timestep, .. } | Layout::Separate { timestep, .. } => {
173 timestep.as_deref()
174 }
175 }
176 }
177
178 fn once(
180 &mut self,
181 a: &[f32],
182 b: &[f32],
183 width: usize,
184 height: usize,
185 fraction: f32,
186 ) -> Result<Vec<f32>, InterpError> {
187 use ndarray::{Array, IxDyn};
188
189 let rgb = |plane: &[f32]| -> Vec<f32> {
192 let mut out = Vec::with_capacity(plane.len() * 3);
193 for _ in 0..3 {
194 out.extend_from_slice(plane);
195 }
196 out
197 };
198 let shaped = |data: Vec<f32>, channels: usize| {
199 Array::from_shape_vec(IxDyn(&[1, channels, height, width]), data)
200 .map_err(|error| InterpError::Model(error.to_string()))
201 };
202
203 let mut inputs = match self.layout.clone() {
204 Layout::Stacked { image, .. } => {
205 let mut both = rgb(a);
206 both.extend(rgb(b));
207 vec![(image, shaped(both, 6)?)]
208 }
209 Layout::Separate { first, second, .. } => vec![
210 (first, shaped(rgb(a), 3)?),
211 (second, shaped(rgb(b), 3)?),
212 ],
213 };
214 if let Some(name) = self.timestep().map(str::to_owned) {
215 inputs.push((
216 name,
217 Array::from_shape_vec(IxDyn(&[1]), vec![fraction])
218 .map_err(|error| InterpError::Model(error.to_string()))?,
219 ));
220 }
221
222 let outputs = self
223 .model
224 .run_named(inputs)
225 .map_err(|error| InterpError::Model(error.to_string()))?;
226 let (_, image) = outputs
227 .into_iter()
228 .next()
229 .ok_or_else(|| InterpError::Model("the graph returned nothing".into()))?;
230 Ok(to_luma(image.as_slice().unwrap_or(&[]), width * height))
231 }
232
233 fn bisect(
236 &mut self,
237 a: &[f32],
238 b: &[f32],
239 width: usize,
240 height: usize,
241 fraction: f32,
242 depth: usize,
243 ) -> Result<Vec<f32>, InterpError> {
244 const MAX_DEPTH: usize = 4;
245 if (fraction - 0.5).abs() < 1e-6 {
246 return self.once(a, b, width, height, 0.5);
247 }
248 if depth >= MAX_DEPTH {
249 return Err(InterpError::Unsupported(format!(
250 "this export has no timestep input, so it can only produce midpoints; a fraction \
251 of {fraction} would need more than {MAX_DEPTH} bisections. Use an interpolation \
252 factor that is a power of two, or export a model that takes a timestep."
253 )));
254 }
255 let middle = self.once(a, b, width, height, 0.5)?;
256 if fraction < 0.5 {
257 self.bisect(a, &middle, width, height, fraction * 2.0, depth + 1)
258 } else {
259 self.bisect(&middle, b, width, height, (fraction - 0.5) * 2.0, depth + 1)
260 }
261 }
262}
263
264#[cfg(feature = "onnx")]
266fn to_luma(data: &[f32], plane: usize) -> Vec<f32> {
267 if plane == 0 {
268 return Vec::new();
269 }
270 let channels = (data.len() / plane).max(1);
271 (0..plane)
272 .map(|index| {
273 let sum: f32 = (0..channels.min(3))
274 .map(|channel| data.get(channel * plane + index).copied().unwrap_or(0.0))
275 .sum();
276 (sum / channels.min(3) as f32).clamp(0.0, 1.0)
277 })
278 .collect()
279}
280
281#[cfg(feature = "onnx")]
282impl FrameInterpolator for OnnxInterpolator {
283 fn between(
284 &mut self,
285 a: &[f32],
286 b: &[f32],
287 width: usize,
288 height: usize,
289 fractions: &[f32],
290 ) -> Result<Vec<Vec<f32>>, InterpError> {
291 let timed = self.timestep().is_some();
292 fractions
293 .iter()
294 .map(|fraction| {
295 if timed {
296 self.once(a, b, width, height, *fraction)
297 } else {
298 self.bisect(a, b, width, height, *fraction, 0)
299 }
300 })
301 .collect()
302 }
303}
304
305pub struct Interpolation<'a> {
309 pub interpolator: &'a mut dyn FrameInterpolator,
310 pub factor: usize,
311}
312
313impl Interpolation<'_> {
314 pub fn fractions(&self) -> Vec<f32> {
316 (1..self.factor.max(1))
317 .map(|step| step as f32 / self.factor as f32)
318 .collect()
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::{FrameInterpolator, Interpolation, LinearInterpolator};
325
326 #[test]
327 fn fractions_split_the_interval_evenly_and_exclude_the_endpoints() {
328 let mut linear = LinearInterpolator;
329 let plan = Interpolation {
330 interpolator: &mut linear,
331 factor: 4,
332 };
333 assert_eq!(plan.fractions(), vec![0.25, 0.5, 0.75]);
334 }
335
336 #[test]
337 fn a_factor_of_one_inserts_nothing() {
338 let mut linear = LinearInterpolator;
339 let plan = Interpolation {
340 interpolator: &mut linear,
341 factor: 1,
342 };
343 assert!(plan.fractions().is_empty());
344 }
345
346 #[test]
347 fn the_linear_baseline_blends_the_way_the_simulator_would() {
348 let mut linear = LinearInterpolator;
349 let frames = linear
350 .between(&[0.0, 1.0], &[1.0, 0.0], 2, 1, &[0.25, 0.75])
351 .unwrap();
352 assert_eq!(frames, vec![vec![0.25, 0.75], vec![0.75, 0.25]]);
353 }
354}