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 op_out_of_plan!();
389
390 fn eval(&self, _ctx: &EvalContext, mut inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
391 let input_dt = inputs[0].datum_type();
392 let scales = self.optional_scales_input.and_then(|ix| inputs.get(ix));
393 let sizes = self.optional_sizes_input.and_then(|ix| inputs.get(ix));
394 let output_shape = self.compute_output_shape(
395 inputs[0].shape(),
396 scales.map(|t| &**t),
397 sizes.map(|t| &**t),
398 )?;
399 let scales: TVec<f32> = if let Some(scales) = scales.filter(|s| s.len() == inputs[0].rank())
400 {
401 scales.try_as_plain()?.as_slice::<f32>()?.into()
402 } else {
403 output_shape.iter().zip(inputs[0].shape()).map(|(o, i)| *o as f32 / *i as f32).collect()
404 };
405 let input = inputs.remove(0).into_tensor();
406 let input = input.cast_to::<f32>()?;
407 let mut shape: TVec<usize> = input.shape().into();
408 let mut data: Vec<f32> = input.try_as_plain()?.as_slice::<f32>()?.to_vec();
409 for (axis, scale) in scales.into_iter().enumerate() {
410 let (len_in, len_out) = (shape[axis], output_shape[axis]);
411 if len_in == len_out && scale == 1.0 {
412 continue;
413 }
414 let plan = self.plan_axis(scale, len_in, len_out);
415 let mut resampled = vec![0f32; data.len() / len_in * len_out];
416 resample_axis(&data, &shape, axis, &plan, 0.0, &mut resampled);
417 data = resampled;
418 shape[axis] = len_out;
419 }
420 let out = tract_ndarray::ArrayD::from_shape_vec(&*shape, data)?.into_tensor();
421 let out =
422 if out.datum_type() == input_dt { out } else { out.cast_to_dt(input_dt)?.into_owned() };
423 Ok(tvec!(out.into_tvalue()))
424 }
425}
426
427impl TypedOp for Resize {
428 as_op!();
429
430 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
431 let scales = self.optional_scales_input.and_then(|ix| inputs.get(ix));
432 let sizes = self.optional_sizes_input.and_then(|ix| inputs.get(ix));
433 let output_shape = self.compute_output_shape(
434 &inputs[0].shape,
435 scales.and_then(|f| f.konst.as_deref()),
436 sizes.and_then(|f| f.konst.as_deref()),
437 )?;
438 Ok(tvec!(inputs[0].datum_type.fact(&output_shape)))
439 }
440
441 fn declutter(
442 &self,
443 model: &TypedModel,
444 node: &TypedNode,
445 ) -> TractResult<Option<TypedModelPatch>> {
446 rule_if!(matches!(self.interpolator, Interpolator::Nearest));
447 rule_if_some!(scales_input = self.optional_scales_input);
448 let scales_fact = model.outlet_fact(node.inputs[scales_input])?;
449 rule_if_some!(scales_tensor = &scales_fact.konst);
450 let scales: Vec<f32> =
451 scales_tensor.cast_to::<f32>()?.try_as_plain()?.as_slice::<f32>()?.to_vec();
452 let int_scales: Vec<usize> = scales.iter().map(|&s| s.round() as usize).collect();
453 rule_if!(
454 scales.iter().zip(&int_scales).all(|(&s, &i)| (s - i as f32).abs() <= 1e-5 && i != 0)
455 );
456 rule_if!(int_scales.iter().any(|&s| s != 1));
457 let input_shape = &model.outlet_fact(node.inputs[0])?.shape;
458 for (axis, &scale) in int_scales.iter().enumerate().filter(|&(_, &s)| s > 1) {
459 let Some(len_in) = probe_length(&self.coord_transformer, &input_shape[axis]) else {
460 return Ok(None);
461 };
462 rule_if!(is_pixel_replication(
463 &self.plan_axis(scale as f32, len_in, len_in * scale),
464 scale
465 ));
466 }
467
468 lower_nearest_integer_upsample(model, node, &int_scales)
469 }
470}
471
472pub fn probe_length(coord_transformer: &CoordTransformer, len: &TDim) -> Option<usize> {
476 len.to_usize().ok().or(match coord_transformer {
477 CoordTransformer::HalfPixel
478 | CoordTransformer::Asymmetric
479 | CoordTransformer::TfHalfPixelForNn => Some(4),
480 _ => None,
481 })
482}
483
484pub fn lower_nearest_integer_upsample(
488 model: &TypedModel,
489 node: &TypedNode,
490 int_scales: &[usize],
491) -> TractResult<Option<TypedModelPatch>> {
492 let input_fact = model.outlet_fact(node.inputs[0])?;
493 let input_shape = &input_fact.shape;
494
495 let mut patch = TypedModelPatch::default();
496 let mut wire = patch.tap_model(model, node.inputs[0])?;
497
498 let mut from_dims: TVec<TDim> = tvec![];
499 let mut to_dims: TVec<TDim> = tvec![];
500 let mut tile_multipliers: TVec<TDim> = tvec![];
501 let mut first_upsampled = None;
502
503 for (i, &scale) in int_scales.iter().enumerate() {
504 from_dims.push(input_shape[i].clone());
505 to_dims.push(input_shape[i].clone());
506 tile_multipliers.push(1.into());
507 if scale > 1 {
508 if first_upsampled.is_none() {
509 first_upsampled = Some(i);
510 }
511 to_dims.push(1.into());
512 tile_multipliers.push(scale.into());
513 }
514 }
515
516 if to_dims.len() > from_dims.len() {
517 let first = first_upsampled.unwrap();
518 wire = patch.wire_node(
519 format!("{}.reshape_pre", node.name),
520 AxisOp::Reshape(first, from_dims[first..].into(), to_dims[first..].into()),
521 &[wire],
522 )?[0];
523 }
524
525 wire = patch.wire_node(
526 format!("{}.tile", node.name),
527 Tile { multipliers: tile_multipliers },
528 &[wire],
529 )?[0];
530
531 let tiled_shape: TVec<TDim> = to_dims
532 .iter()
533 .zip(int_scales.iter().flat_map(|&s| if s > 1 { vec![1usize, s] } else { vec![1] }))
534 .map(|(d, s)| d.clone() * s)
535 .collect();
536 let mut final_dims: TVec<TDim> = tvec![];
537 let mut idx = 0;
538 for &scale in int_scales {
539 if scale > 1 {
540 final_dims.push(tiled_shape[idx].clone() * tiled_shape[idx + 1].clone());
541 idx += 2;
542 } else {
543 final_dims.push(tiled_shape[idx].clone());
544 idx += 1;
545 }
546 }
547
548 if tiled_shape.len() > final_dims.len() {
549 let first = first_upsampled.unwrap();
550 wire = patch.wire_node(
551 format!("{}.reshape_post", node.name),
552 AxisOp::Reshape(first, tiled_shape[first..].into(), final_dims[first..].into()),
553 &[wire],
554 )?[0];
555 }
556
557 patch.shunt_outside(model, node.id.into(), wire)?;
558 Ok(Some(patch))
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
566 fn cubic_kernel_properties() {
567 let a = -0.75f32;
568 assert!((cubic_kernel(0.0, a) - 1.0).abs() < 1e-6);
569 assert!(cubic_kernel(2.0, a).abs() < 1e-6);
570 assert!(cubic_kernel(3.0, a).abs() < 1e-6);
571
572 for t_int in 0..=100 {
573 let t = t_int as f32 / 100.0;
574 let sum = cubic_kernel(t + 1.0, a)
575 + cubic_kernel(t, a)
576 + cubic_kernel(1.0 - t, a)
577 + cubic_kernel(2.0 - t, a);
578 assert!((sum - 1.0).abs() < 1e-5, "kernel weights must sum to 1.0, got {sum} at t={t}");
579 }
580 }
581
582 fn cubic_resize(input: Tensor, scales: &[f32]) -> Tensor {
583 let scales = tract_ndarray::Array1::from(scales.to_vec()).into_tensor();
584 let op = Resize {
585 coord_transformer: CoordTransformer::HalfPixel,
586 interpolator: Interpolator::Cubic,
587 nearest: Nearest::Floor,
588 optional_scales_input: Some(1),
589 optional_sizes_input: None,
590 };
591 op.eval(&EvalContext::out_of_plan(), tvec!(input.into_tvalue(), scales.into_tvalue()))
592 .unwrap()
593 .remove(0)
594 .into_tensor()
595 }
596
597 #[test]
598 fn cubic_resize_1d_upsample() {
599 let out = cubic_resize(tract_ndarray::arr1(&[0.0f32, 1.0, 2.0, 3.0]).into_tensor(), &[2.0]);
600 let plain = out.try_as_plain().unwrap();
601 let output = plain.as_slice::<f32>().unwrap();
602 assert_eq!(output.len(), 8);
603 assert!((output[0] - (-0.10546875)).abs() < 1e-4, "got {}", output[0]);
604 }
605
606 #[test]
607 fn cubic_resize_2d_upsample() {
608 let out = cubic_resize(
609 tract_ndarray::arr2(&[[1.0f32, 2.0], [3.0, 4.0]]).into_tensor(),
610 &[2.0, 2.0],
611 );
612 assert_eq!(out.shape(), &[4, 4]);
613 }
614
615 fn replicates(coord_transformer: CoordTransformer, nearest: Nearest, scale: usize) -> bool {
616 let op = Resize {
617 coord_transformer,
618 interpolator: Interpolator::Nearest,
619 nearest,
620 optional_scales_input: Some(1),
621 optional_sizes_input: None,
622 };
623 is_pixel_replication(&op.plan_axis(scale as f32, 4, 4 * scale), scale)
624 }
625
626 #[test]
627 fn only_some_nearest_modes_replicate_pixels() {
628 assert!(replicates(CoordTransformer::Asymmetric, Nearest::Floor, 2));
629 assert!(replicates(CoordTransformer::HalfPixel, Nearest::RoundPreferCeil, 2));
630 assert!(replicates(CoordTransformer::HalfPixel, Nearest::RoundPreferCeil, 3));
631 assert!(!replicates(CoordTransformer::HalfPixel, Nearest::Floor, 2));
632 assert!(!replicates(CoordTransformer::Asymmetric, Nearest::RoundPreferCeil, 2));
633 }
634}