1use crate::internal::*;
2use crate::ops::array::Tile;
3
4#[derive(Clone, Debug, Hash, PartialEq, Eq)]
7pub enum CoordTransformer {
8 HalfPixel,
9 AlignCorners,
10 Asymmetric,
11 PytorchHalfPixel,
12 HalfPixelSymmetric,
13 TfHalfPixelForNn,
14}
15
16impl CoordTransformer {
17 pub fn transform(&self, x_out: usize, scale: f32, len_in: usize, len_out: usize) -> f32 {
18 match self {
19 CoordTransformer::HalfPixel => (x_out as f32 + 0.5) / scale - 0.5,
20 CoordTransformer::AlignCorners => {
21 let output_width = scale * len_in as f32;
22 if output_width == 1.0 {
23 0.0
24 } else {
25 (x_out as f32 * (len_in as f32 - 1.0)) / (output_width - 1.0)
26 }
27 }
28 CoordTransformer::Asymmetric => (x_out as f32) / scale,
29 CoordTransformer::PytorchHalfPixel => {
30 if len_out > 1 {
31 (x_out as f32 + 0.5) / scale - 0.5
32 } else {
33 -0.5
34 }
35 }
36 CoordTransformer::HalfPixelSymmetric => {
37 let adjustment = len_out as f32 / (scale * len_in as f32);
38 let offset = len_in as f32 / 2.0 * (1.0 - adjustment);
39 offset + (x_out as f32 + 0.5) / scale - 0.5
40 }
41 CoordTransformer::TfHalfPixelForNn => (x_out as f32 + 0.5) / scale,
42 }
43 }
44
45 pub fn as_str(&self) -> &'static str {
46 match self {
47 CoordTransformer::HalfPixel => "half_pixel",
48 CoordTransformer::AlignCorners => "align_corners",
49 CoordTransformer::Asymmetric => "asymmetric",
50 CoordTransformer::PytorchHalfPixel => "pytorch_half_pixel",
51 CoordTransformer::HalfPixelSymmetric => "half_pixel_symmetric",
52 CoordTransformer::TfHalfPixelForNn => "tf_half_pixel_for_nn",
53 }
54 }
55
56 pub fn parse(s: &str) -> TractResult<Self> {
57 Ok(match s {
58 "half_pixel" => CoordTransformer::HalfPixel,
59 "align_corners" => CoordTransformer::AlignCorners,
60 "asymmetric" => CoordTransformer::Asymmetric,
61 "pytorch_half_pixel" => CoordTransformer::PytorchHalfPixel,
62 "half_pixel_symmetric" => CoordTransformer::HalfPixelSymmetric,
63 "tf_half_pixel_for_nn" => CoordTransformer::TfHalfPixelForNn,
64 s => bail!("coordinate_transformation_mode: {s}"),
65 })
66 }
67}
68
69#[derive(Clone, Debug, Hash, PartialEq, Eq)]
72pub enum Interpolator {
73 Linear,
74 Nearest,
75 Cubic,
76}
77
78impl Interpolator {
79 pub fn as_str(&self) -> &'static str {
80 match self {
81 Interpolator::Linear => "linear",
82 Interpolator::Nearest => "nearest",
83 Interpolator::Cubic => "cubic",
84 }
85 }
86
87 pub fn parse(s: &str) -> TractResult<Self> {
88 Ok(match s {
89 "linear" => Interpolator::Linear,
90 "nearest" => Interpolator::Nearest,
91 "cubic" => Interpolator::Cubic,
92 s => bail!("mode: {s}"),
93 })
94 }
95}
96
97pub fn window_size(interpolator: &Interpolator, antialias: bool, scale: f32) -> usize {
101 let support = match interpolator {
102 Interpolator::Nearest | Interpolator::Linear => 1.0f32,
103 Interpolator::Cubic => 2.0,
104 };
105 if !antialias || scale >= 1.0 || matches!(interpolator, Interpolator::Nearest) {
106 return 2 * support as usize;
107 }
108 let first = (-support / scale).floor() as isize + 1;
109 (2 - 2 * first) as usize
110}
111
112pub fn linear_weights(r: f32, scale: f32, antialias: bool, weights: &mut [f32]) {
114 let scale = if antialias { scale.min(1.0) } else { 1.0 };
115 fill_weights(r, scale, weights, |x| (1.0 - x.abs()).clamp(0.0, 1.0));
116}
117
118pub fn cubic_weights(r: f32, scale: f32, a: f32, antialias: bool, weights: &mut [f32]) {
121 let scale = if antialias { scale.min(1.0) } else { 1.0 };
122 fill_weights(r, scale, weights, |x| cubic_kernel(x, a));
123}
124
125fn fill_weights(r: f32, scale: f32, weights: &mut [f32], kernel: impl Fn(f32) -> f32) {
128 let first = 1.0 - (weights.len() / 2) as f32;
129 for (k, w) in weights.iter_mut().enumerate() {
130 *w = kernel((first + k as f32 - r) * scale);
131 }
132 if scale != 1.0 {
133 let sum: f32 = weights.iter().sum();
134 weights.iter_mut().for_each(|w| *w /= sum);
135 }
136}
137
138pub fn cubic_kernel(s: f32, a: f32) -> f32 {
140 let abs_s = s.abs();
141 if abs_s <= 1.0 {
142 (a + 2.0) * abs_s * abs_s * abs_s - (a + 3.0) * abs_s * abs_s + 1.0
143 } else if abs_s <= 2.0 {
144 a * abs_s * abs_s * abs_s - 5.0 * a * abs_s * abs_s + 8.0 * a * abs_s - 4.0 * a
145 } else {
146 0.0
147 }
148}
149
150#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
154pub enum Nearest {
155 Floor,
156 RoundPreferCeil,
157}
158
159impl Nearest {
160 pub fn prefers_right(&self, x_ratio: f32) -> bool {
162 match self {
163 Nearest::Floor => false,
164 Nearest::RoundPreferCeil => x_ratio >= 0.5,
165 }
166 }
167
168 pub fn as_str(&self) -> &'static str {
169 match self {
170 Nearest::Floor => "floor",
171 Nearest::RoundPreferCeil => "round_prefer_ceil",
172 }
173 }
174
175 pub fn parse(s: &str) -> TractResult<Self> {
176 Ok(match s {
177 "floor" => Nearest::Floor,
178 "round_prefer_ceil" => Nearest::RoundPreferCeil,
179 s => bail!("nearest_mode: {s}"),
180 })
181 }
182}
183
184#[derive(Clone, Debug)]
189pub struct AxisPlan {
190 pub window: usize,
191 pub indices: Vec<usize>,
192 pub weights: Vec<f32>,
193 pub extrapolated: Vec<bool>,
194}
195
196pub fn plan_axis(
203 len_in: usize,
204 len_out: usize,
205 window: usize,
206 exclude_outside: bool,
207 coord: impl Fn(usize) -> Option<f32>,
208 weights: impl Fn(f32, &mut [f32]),
209) -> AxisPlan {
210 let mut plan = AxisPlan {
211 window,
212 indices: vec![0; window * len_out],
213 weights: vec![0.0; window * len_out],
214 extrapolated: vec![false; len_out],
215 };
216 for x in 0..len_out {
217 let Some(x_in) = coord(x) else {
218 plan.extrapolated[x] = true;
219 continue;
220 };
221 let cell = x_in.ceil() - 1.0;
222 let taps = &mut plan.weights[x * window..][..window];
223 weights(x_in - cell, taps);
224 let first = cell as isize + 1 - (window / 2) as isize;
225 for (k, tap) in taps.iter_mut().enumerate() {
226 let raw = first + k as isize;
227 if exclude_outside && (raw < 0 || raw >= len_in as isize) {
228 *tap = 0.0;
229 }
230 plan.indices[x * window + k] = raw.clamp(0, len_in as isize - 1) as usize;
231 }
232 if exclude_outside {
233 let sum: f32 = taps.iter().sum();
234 if sum != 0.0 {
235 taps.iter_mut().for_each(|w| *w /= sum);
236 }
237 }
238 }
239 plan
240}
241
242pub fn is_pixel_replication(plan: &AxisPlan, scale: usize) -> bool {
247 !plan.extrapolated.contains(&true)
248 && plan
249 .indices
250 .chunks_exact(plan.window)
251 .zip(plan.weights.chunks_exact(plan.window))
252 .enumerate()
253 .all(|(x, (indices, weights))| {
254 let mut taps = indices.iter().zip(weights).filter(|(_, w)| **w != 0.0);
255 taps.next().is_some_and(|(i, w)| *w == 1.0 && *i == x / scale)
256 && taps.next().is_none()
257 })
258}
259
260pub fn resample_axis(
263 input: &[f32],
264 shape: &[usize],
265 axis: usize,
266 plan: &AxisPlan,
267 extrapolation_value: f32,
268 output: &mut [f32],
269) {
270 let len_in = shape[axis];
271 let len_out = plan.extrapolated.len();
272 let inner: usize = shape[axis + 1..].iter().product();
273 let window = plan.window;
274 if len_in * inner == 0 || len_out * inner == 0 {
275 return;
276 }
277 for (src, dst) in
278 input.chunks_exact(len_in * inner).zip(output.chunks_exact_mut(len_out * inner))
279 {
280 for (x, dst) in dst.chunks_exact_mut(inner).enumerate() {
281 if plan.extrapolated[x] {
282 dst.fill(extrapolation_value);
283 continue;
284 }
285 dst.fill(0.0);
286 let indices = &plan.indices[x * window..][..window];
287 let weights = &plan.weights[x * window..][..window];
288 for (&i, &w) in indices.iter().zip(weights) {
289 if w == 0.0 {
290 continue;
291 }
292 for (d, s) in dst.iter_mut().zip(&src[i * inner..][..inner]) {
293 *d += w * s;
294 }
295 }
296 }
297 }
298}
299
300#[derive(Clone, Debug, Hash, PartialEq, Eq)]
305pub struct Resize {
306 pub coord_transformer: CoordTransformer,
307 pub interpolator: Interpolator,
308 pub nearest: Nearest,
309 pub optional_scales_input: Option<usize>,
310 pub optional_sizes_input: Option<usize>,
311}
312
313impl Resize {
314 pub fn compute_output_shape<D: DimLike>(
315 &self,
316 input_shape: &[D],
317 input_scale: Option<&Tensor>,
318 input_sizes: Option<&Tensor>,
319 ) -> TractResult<TVec<D>> {
320 if let Some(scale) = input_scale
321 && scale.len() == input_shape.len()
322 {
323 let mut shape = tvec!();
324 for (i, s) in input_shape
325 .iter()
326 .zip(scale.cast_to::<f32>()?.try_as_plain()?.as_slice::<f32>()?.iter())
327 {
328 if s.round() == *s {
329 shape.push(i.clone() * (*s as usize));
330 } else if let Ok(i) = i.to_usize() {
331 shape.push(((i as f32 * s) as usize).into());
332 } else {
333 bail!(
334 "Can not compute output shape. inputs are {input_shape:?} and scale {scale:?}"
335 )
336 }
337 }
338 return Ok(shape);
339 }
340 if let Some(sizes) = input_sizes
341 && sizes.len() == input_shape.len()
342 {
343 return sizes
344 .cast_to::<TDim>()?
345 .try_as_plain()?
346 .as_slice::<TDim>()?
347 .iter()
348 .map(|i| i.try_into())
349 .collect();
350 }
351 bail!(
352 "Neither sizes nor scales makes sense: input_shape: {:?}, scale: {:?}, sizes: {:?}",
353 input_shape,
354 input_scale,
355 input_sizes,
356 );
357 }
358
359 pub fn plan_axis(&self, scale: f32, len_in: usize, len_out: usize) -> AxisPlan {
361 let window = window_size(&self.interpolator, false, scale);
362 let coord = |x| Some(self.coord_transformer.transform(x, scale, len_in, len_out));
363 match self.interpolator {
364 Interpolator::Linear => plan_axis(len_in, len_out, window, false, coord, |r, w| {
365 linear_weights(r, scale, false, w)
366 }),
367 Interpolator::Cubic => plan_axis(len_in, len_out, window, false, coord, |r, w| {
368 cubic_weights(r, scale, -0.75, false, w)
369 }),
370 Interpolator::Nearest => plan_axis(len_in, len_out, window, false, coord, |r, w| {
371 let right = r == 1.0 || self.nearest.prefers_right(r);
372 w[0] = !right as u8 as f32;
373 w[1] = right as u8 as f32;
374 }),
375 }
376 }
377}
378
379impl Op for Resize {
380 fn name(&self) -> StaticName {
381 "Resize".into()
382 }
383
384 op_as_typed_op!();
385}
386
387impl EvalOp for Resize {
388 fn is_stateless(&self) -> bool {
389 true
390 }
391
392 fn eval(&self, mut inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
393 let input_dt = inputs[0].datum_type();
394 let scales = self.optional_scales_input.and_then(|ix| inputs.get(ix));
395 let sizes = self.optional_sizes_input.and_then(|ix| inputs.get(ix));
396 let output_shape = self.compute_output_shape(
397 inputs[0].shape(),
398 scales.map(|t| &**t),
399 sizes.map(|t| &**t),
400 )?;
401 let scales: TVec<f32> = if let Some(scales) = scales.filter(|s| s.len() == inputs[0].rank())
402 {
403 scales.try_as_plain()?.as_slice::<f32>()?.into()
404 } else {
405 output_shape.iter().zip(inputs[0].shape()).map(|(o, i)| *o as f32 / *i as f32).collect()
406 };
407 let input = inputs.remove(0).into_tensor();
408 let input = input.cast_to::<f32>()?;
409 let mut shape: TVec<usize> = input.shape().into();
410 let mut data: Vec<f32> = input.try_as_plain()?.as_slice::<f32>()?.to_vec();
411 for (axis, scale) in scales.into_iter().enumerate() {
412 let (len_in, len_out) = (shape[axis], output_shape[axis]);
413 if len_in == len_out && scale == 1.0 {
414 continue;
415 }
416 let plan = self.plan_axis(scale, len_in, len_out);
417 let mut resampled = vec![0f32; data.len() / len_in * len_out];
418 resample_axis(&data, &shape, axis, &plan, 0.0, &mut resampled);
419 data = resampled;
420 shape[axis] = len_out;
421 }
422 let out = tract_ndarray::ArrayD::from_shape_vec(&*shape, data)?.into_tensor();
423 let out =
424 if out.datum_type() == input_dt { out } else { out.cast_to_dt(input_dt)?.into_owned() };
425 Ok(tvec!(out.into_tvalue()))
426 }
427}
428
429impl TypedOp for Resize {
430 as_op!();
431
432 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
433 let scales = self.optional_scales_input.and_then(|ix| inputs.get(ix));
434 let sizes = self.optional_sizes_input.and_then(|ix| inputs.get(ix));
435 let output_shape = self.compute_output_shape(
436 &inputs[0].shape,
437 scales.and_then(|f| f.konst.as_deref()),
438 sizes.and_then(|f| f.konst.as_deref()),
439 )?;
440 Ok(tvec!(inputs[0].datum_type.fact(&output_shape)))
441 }
442
443 fn declutter(
444 &self,
445 model: &TypedModel,
446 node: &TypedNode,
447 ) -> TractResult<Option<TypedModelPatch>> {
448 rule_if!(matches!(self.interpolator, Interpolator::Nearest));
449 rule_if_some!(scales_input = self.optional_scales_input);
450 let scales_fact = model.outlet_fact(node.inputs[scales_input])?;
451 rule_if_some!(scales_tensor = &scales_fact.konst);
452 let scales: Vec<f32> =
453 scales_tensor.cast_to::<f32>()?.try_as_plain()?.as_slice::<f32>()?.to_vec();
454 let int_scales: Vec<usize> = scales.iter().map(|&s| s.round() as usize).collect();
455 rule_if!(
456 scales.iter().zip(&int_scales).all(|(&s, &i)| (s - i as f32).abs() <= 1e-5 && i != 0)
457 );
458 rule_if!(int_scales.iter().any(|&s| s != 1));
459 let input_shape = &model.outlet_fact(node.inputs[0])?.shape;
460 for (axis, &scale) in int_scales.iter().enumerate().filter(|&(_, &s)| s > 1) {
461 let Some(len_in) = probe_length(&self.coord_transformer, &input_shape[axis]) else {
462 return Ok(None);
463 };
464 rule_if!(is_pixel_replication(
465 &self.plan_axis(scale as f32, len_in, len_in * scale),
466 scale
467 ));
468 }
469
470 lower_nearest_integer_upsample(model, node, &int_scales)
471 }
472}
473
474pub fn probe_length(coord_transformer: &CoordTransformer, len: &TDim) -> Option<usize> {
478 len.to_usize().ok().or(match coord_transformer {
479 CoordTransformer::HalfPixel
480 | CoordTransformer::Asymmetric
481 | CoordTransformer::TfHalfPixelForNn => Some(4),
482 _ => None,
483 })
484}
485
486pub fn lower_nearest_integer_upsample(
490 model: &TypedModel,
491 node: &TypedNode,
492 int_scales: &[usize],
493) -> TractResult<Option<TypedModelPatch>> {
494 let input_fact = model.outlet_fact(node.inputs[0])?;
495 let input_shape = &input_fact.shape;
496
497 let mut patch = TypedModelPatch::default();
498 let mut wire = patch.tap_model(model, node.inputs[0])?;
499
500 let mut from_dims: TVec<TDim> = tvec![];
501 let mut to_dims: TVec<TDim> = tvec![];
502 let mut tile_multipliers: TVec<TDim> = tvec![];
503 let mut first_upsampled = None;
504
505 for (i, &scale) in int_scales.iter().enumerate() {
506 from_dims.push(input_shape[i].clone());
507 to_dims.push(input_shape[i].clone());
508 tile_multipliers.push(1.into());
509 if scale > 1 {
510 if first_upsampled.is_none() {
511 first_upsampled = Some(i);
512 }
513 to_dims.push(1.into());
514 tile_multipliers.push(scale.into());
515 }
516 }
517
518 if to_dims.len() > from_dims.len() {
519 let first = first_upsampled.unwrap();
520 wire = patch.wire_node(
521 format!("{}.reshape_pre", node.name),
522 AxisOp::Reshape(first, from_dims[first..].into(), to_dims[first..].into()),
523 &[wire],
524 )?[0];
525 }
526
527 wire = patch.wire_node(
528 format!("{}.tile", node.name),
529 Tile { multipliers: tile_multipliers },
530 &[wire],
531 )?[0];
532
533 let tiled_shape: TVec<TDim> = to_dims
534 .iter()
535 .zip(int_scales.iter().flat_map(|&s| if s > 1 { vec![1usize, s] } else { vec![1] }))
536 .map(|(d, s)| d.clone() * s)
537 .collect();
538 let mut final_dims: TVec<TDim> = tvec![];
539 let mut idx = 0;
540 for &scale in int_scales {
541 if scale > 1 {
542 final_dims.push(tiled_shape[idx].clone() * tiled_shape[idx + 1].clone());
543 idx += 2;
544 } else {
545 final_dims.push(tiled_shape[idx].clone());
546 idx += 1;
547 }
548 }
549
550 if tiled_shape.len() > final_dims.len() {
551 let first = first_upsampled.unwrap();
552 wire = patch.wire_node(
553 format!("{}.reshape_post", node.name),
554 AxisOp::Reshape(first, tiled_shape[first..].into(), final_dims[first..].into()),
555 &[wire],
556 )?[0];
557 }
558
559 patch.shunt_outside(model, node.id.into(), wire)?;
560 Ok(Some(patch))
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn cubic_kernel_properties() {
569 let a = -0.75f32;
570 assert!((cubic_kernel(0.0, a) - 1.0).abs() < 1e-6);
571 assert!(cubic_kernel(2.0, a).abs() < 1e-6);
572 assert!(cubic_kernel(3.0, a).abs() < 1e-6);
573
574 for t_int in 0..=100 {
575 let t = t_int as f32 / 100.0;
576 let sum = cubic_kernel(t + 1.0, a)
577 + cubic_kernel(t, a)
578 + cubic_kernel(1.0 - t, a)
579 + cubic_kernel(2.0 - t, a);
580 assert!((sum - 1.0).abs() < 1e-5, "kernel weights must sum to 1.0, got {sum} at t={t}");
581 }
582 }
583
584 fn cubic_resize(input: Tensor, scales: &[f32]) -> Tensor {
585 let scales = tract_ndarray::Array1::from(scales.to_vec()).into_tensor();
586 let op = Resize {
587 coord_transformer: CoordTransformer::HalfPixel,
588 interpolator: Interpolator::Cubic,
589 nearest: Nearest::Floor,
590 optional_scales_input: Some(1),
591 optional_sizes_input: None,
592 };
593 op.eval(tvec!(input.into_tvalue(), scales.into_tvalue())).unwrap().remove(0).into_tensor()
594 }
595
596 #[test]
597 fn cubic_resize_1d_upsample() {
598 let out = cubic_resize(tract_ndarray::arr1(&[0.0f32, 1.0, 2.0, 3.0]).into_tensor(), &[2.0]);
599 let plain = out.try_as_plain().unwrap();
600 let output = plain.as_slice::<f32>().unwrap();
601 assert_eq!(output.len(), 8);
602 assert!((output[0] - (-0.10546875)).abs() < 1e-4, "got {}", output[0]);
603 }
604
605 #[test]
606 fn cubic_resize_2d_upsample() {
607 let out = cubic_resize(
608 tract_ndarray::arr2(&[[1.0f32, 2.0], [3.0, 4.0]]).into_tensor(),
609 &[2.0, 2.0],
610 );
611 assert_eq!(out.shape(), &[4, 4]);
612 }
613
614 fn replicates(coord_transformer: CoordTransformer, nearest: Nearest, scale: usize) -> bool {
615 let op = Resize {
616 coord_transformer,
617 interpolator: Interpolator::Nearest,
618 nearest,
619 optional_scales_input: Some(1),
620 optional_sizes_input: None,
621 };
622 is_pixel_replication(&op.plan_axis(scale as f32, 4, 4 * scale), scale)
623 }
624
625 #[test]
626 fn only_some_nearest_modes_replicate_pixels() {
627 assert!(replicates(CoordTransformer::Asymmetric, Nearest::Floor, 2));
628 assert!(replicates(CoordTransformer::HalfPixel, Nearest::RoundPreferCeil, 2));
629 assert!(replicates(CoordTransformer::HalfPixel, Nearest::RoundPreferCeil, 3));
630 assert!(!replicates(CoordTransformer::HalfPixel, Nearest::Floor, 2));
631 assert!(!replicates(CoordTransformer::Asymmetric, Nearest::RoundPreferCeil, 2));
632 }
633}