1use crate::internal::*;
2use crate::ops::array::MultiBroadcastTo;
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_ram()?.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_ram()?
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_ram()?.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_ram()?.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_ram()?.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
472#[derive(Debug, Clone, Hash, PartialEq, Eq)]
475pub struct NearestUpsample {
476 pub scales: TVec<usize>,
477}
478
479impl Op for NearestUpsample {
480 fn name(&self) -> StaticName {
481 "NearestUpsample".into()
482 }
483
484 fn info(&self) -> TractResult<Vec<String>> {
485 Ok(vec![format!("scales:{:?}", self.scales)])
486 }
487
488 op_as_typed_op!();
489}
490
491impl EvalOp for NearestUpsample {
492 op_out_of_plan!();
493
494 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
495 let input = args_1!(inputs);
496 ensure!(input.rank() == self.scales.len());
497 let out_shape: TVec<usize> =
498 input.shape().iter().zip(self.scales.iter()).map(|(d, s)| d * s).collect();
499 if input.datum_type() == f32::datum_type()
500 && let Some(out) = nearest_hw_f32(&input, &self.scales, &out_shape)?
501 {
502 return Ok(tvec!(out.into_tvalue()));
503 }
504 dispatch_copy!(nearest_generic(input.datum_type())(&input, &self.scales, &out_shape))
505 }
506}
507
508impl TypedOp for NearestUpsample {
509 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
510 ensure!(inputs[0].rank() == self.scales.len());
511 let shape: TVec<TDim> =
512 inputs[0].shape.iter().zip(self.scales.iter()).map(|(d, s)| d.clone() * *s).collect();
513 Ok(tvec!(inputs[0].datum_type.fact(shape)))
514 }
515
516 as_op!();
517}
518
519fn nearest_hw_f32(
520 input: &Tensor,
521 scales: &[usize],
522 out_shape: &[usize],
523) -> TractResult<Option<Tensor>> {
524 if scales.len() < 2 {
525 return Ok(None);
526 }
527 if scales[..scales.len() - 2].iter().any(|&s| s != 1) {
528 return Ok(None);
529 }
530 let sh = scales[scales.len() - 2];
531 let sw = scales[scales.len() - 1];
532 if sh == 1 && sw == 1 {
533 return Ok(None);
534 }
535 if *input.strides().last().unwrap_or(&1) != 1 {
536 return Ok(None);
537 }
538 let h = input.shape()[input.rank() - 2];
539 let w = input.shape()[input.rank() - 1];
540 let planes = input.len() / (h * w);
541 let mut output = unsafe { Tensor::uninitialized::<f32>(out_shape)? };
542 unsafe {
543 let ip = input.as_ptr::<f32>()?;
544 let op = output.as_ptr_mut::<f32>()?;
545 let ow = w * sw;
546 for p in 0..planes {
547 let src = ip.add(p * h * w);
548 let dst = op.add(p * h * sh * ow);
549 for y in 0..h {
550 let srow = src.add(y * w);
551 let drow = dst.add(y * sh * ow);
552 expand_row_f32(srow, drow, w, sw);
553 for ry in 1..sh {
554 std::ptr::copy_nonoverlapping(drow, drow.add(ry * ow), ow);
555 }
556 }
557 }
558 }
559 Ok(Some(output))
560}
561
562unsafe fn expand_row_f32(src: *const f32, dst: *mut f32, w: usize, sw: usize) {
563 unsafe {
564 if sw == 2 {
565 let mut x = 0usize;
566 #[cfg(target_arch = "aarch64")]
567 {
568 use std::arch::aarch64::*;
569 while x + 4 <= w {
570 let v = vld1q_f32(src.add(x));
571 vst2q_f32(dst.add(2 * x), float32x4x2_t(v, v));
572 x += 4;
573 }
574 }
575 while x < w {
576 let v = *src.add(x);
577 *dst.add(2 * x) = v;
578 *dst.add(2 * x + 1) = v;
579 x += 1;
580 }
581 return;
582 }
583 for x in 0..w {
584 let v = *src.add(x);
585 for rx in 0..sw {
586 *dst.add(x * sw + rx) = v;
587 }
588 }
589 }
590}
591
592fn nearest_generic<T: Datum + Copy>(
593 input: &Tensor,
594 scales: &[usize],
595 out_shape: &[usize],
596) -> TractResult<TVec<TValue>> {
597 let plain = input.try_as_plain_ram()?;
598 let src = plain.as_slice::<T>()?;
599 let mut output = unsafe { Tensor::uninitialized::<T>(out_shape)? };
600 let rank = out_shape.len();
601 let mut in_strides = vec![1usize; rank];
602 for i in (0..rank - 1).rev() {
603 in_strides[i] = in_strides[i + 1] * input.shape()[i + 1];
604 }
605 let mut out_strides = vec![1usize; rank];
606 for i in (0..rank - 1).rev() {
607 out_strides[i] = out_strides[i + 1] * out_shape[i + 1];
608 }
609 {
610 let mut out_plain = output.try_as_plain_ram_mut()?;
611 let dst = out_plain.as_slice_mut::<T>()?;
612 for (i, slot) in dst.iter_mut().enumerate() {
613 let mut rem = i;
614 let mut src_ix = 0usize;
615 for ax in 0..rank {
616 let c = rem / out_strides[ax];
617 rem %= out_strides[ax];
618 src_ix += (c / scales[ax]) * in_strides[ax];
619 }
620 *slot = src[src_ix];
621 }
622 }
623 Ok(tvec!(output.into_tvalue()))
624}
625
626pub fn probe_length(coord_transformer: &CoordTransformer, len: &TDim) -> Option<usize> {
630 len.to_usize().ok().or(match coord_transformer {
631 CoordTransformer::HalfPixel
632 | CoordTransformer::Asymmetric
633 | CoordTransformer::TfHalfPixelForNn => Some(4),
634 _ => None,
635 })
636}
637
638pub fn lower_nearest_integer_upsample(
641 model: &TypedModel,
642 node: &TypedNode,
643 int_scales: &[usize],
644) -> TractResult<Option<TypedModelPatch>> {
645 let op = NearestUpsample { scales: int_scales.iter().cloned().collect() };
646 TypedModelPatch::replace_single_op(model, node, &node.inputs[..1], op).map(Some)
647}
648
649pub fn rewrite_nearest_upsample_to_broadcast(
654 _ctx: &(),
655 model: &TypedModel,
656 node: &TypedNode,
657 _name: &str,
658 op: &NearestUpsample,
659) -> TractResult<Option<TypedModelPatch>> {
660 let input_shape = &model.outlet_fact(node.inputs[0])?.shape;
661
662 let mut patch = TypedModelPatch::default();
663 let mut wire = patch.tap_model(model, node.inputs[0])?;
664
665 let mut from_dims: TVec<TDim> = tvec![];
666 let mut to_dims: TVec<TDim> = tvec![];
667 let mut first_upsampled = None;
668
669 for (i, &scale) in op.scales.iter().enumerate() {
670 from_dims.push(input_shape[i].clone());
671 to_dims.push(input_shape[i].clone());
672 if scale > 1 {
673 if first_upsampled.is_none() {
674 first_upsampled = Some(i);
675 }
676 to_dims.push(1.into());
677 }
678 }
679
680 let Some(first) = first_upsampled else { return Ok(None) };
681
682 wire = patch.wire_node(
683 format!("{}.reshape_pre", node.name),
684 AxisOp::Reshape(first, from_dims[first..].into(), to_dims[first..].into()),
685 &[wire],
686 )?[0];
687
688 let tiled_shape: TVec<TDim> = to_dims
689 .iter()
690 .zip(op.scales.iter().flat_map(|&s| if s > 1 { vec![1usize, s] } else { vec![1] }))
691 .map(|(d, s)| d.clone() * s)
692 .collect();
693
694 wire = patch.wire_node(
695 format!("{}.broadcast", node.name),
696 MultiBroadcastTo { shape: tiled_shape.clone().into() },
697 &[wire],
698 )?[0];
699 let mut final_dims: TVec<TDim> = tvec![];
700 let mut idx = 0;
701 for &scale in &op.scales {
702 if scale > 1 {
703 final_dims.push(tiled_shape[idx].clone() * tiled_shape[idx + 1].clone());
704 idx += 2;
705 } else {
706 final_dims.push(tiled_shape[idx].clone());
707 idx += 1;
708 }
709 }
710
711 wire = patch.wire_node(
712 format!("{}.reshape_post", node.name),
713 AxisOp::Reshape(first, tiled_shape[first..].into(), final_dims[first..].into()),
714 &[wire],
715 )?[0];
716
717 patch.shunt_outside(model, node.id.into(), wire)?;
718 Ok(Some(patch))
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724
725 #[test]
726 fn cubic_kernel_properties() {
727 let a = -0.75f32;
728 assert!((cubic_kernel(0.0, a) - 1.0).abs() < 1e-6);
729 assert!(cubic_kernel(2.0, a).abs() < 1e-6);
730 assert!(cubic_kernel(3.0, a).abs() < 1e-6);
731
732 for t_int in 0..=100 {
733 let t = t_int as f32 / 100.0;
734 let sum = cubic_kernel(t + 1.0, a)
735 + cubic_kernel(t, a)
736 + cubic_kernel(1.0 - t, a)
737 + cubic_kernel(2.0 - t, a);
738 assert!((sum - 1.0).abs() < 1e-5, "kernel weights must sum to 1.0, got {sum} at t={t}");
739 }
740 }
741
742 #[test]
743 fn nearest_upsample_2x2_replicates_pixels() {
744 let src = Tensor::from_shape(&[1, 1, 2, 2], &[1.0f32, 2.0, 3.0, 4.0]).unwrap();
745 let op = NearestUpsample { scales: tvec![1, 1, 2, 2] };
746 let out = op.eval(&EvalContext::out_of_plan(), tvec!(src.into_tvalue())).unwrap().remove(0);
747 let v = out.to_plain_array_view::<f32>().unwrap();
748 assert_eq!(v.shape(), &[1, 1, 4, 4]);
749 assert_eq!(v[[0, 0, 0, 0]], 1.0);
750 assert_eq!(v[[0, 0, 0, 1]], 1.0);
751 assert_eq!(v[[0, 0, 1, 0]], 1.0);
752 assert_eq!(v[[0, 0, 1, 1]], 1.0);
753 assert_eq!(v[[0, 0, 2, 2]], 4.0);
754 assert_eq!(v[[0, 0, 3, 3]], 4.0);
755 assert_eq!(v[[0, 0, 0, 2]], 2.0);
756 assert_eq!(v[[0, 0, 2, 0]], 3.0);
757 }
758
759 fn cubic_resize(input: Tensor, scales: &[f32]) -> Tensor {
760 let scales = tract_ndarray::Array1::from(scales.to_vec()).into_tensor();
761 let op = Resize {
762 coord_transformer: CoordTransformer::HalfPixel,
763 interpolator: Interpolator::Cubic,
764 nearest: Nearest::Floor,
765 optional_scales_input: Some(1),
766 optional_sizes_input: None,
767 };
768 op.eval(&EvalContext::out_of_plan(), tvec!(input.into_tvalue(), scales.into_tvalue()))
769 .unwrap()
770 .remove(0)
771 .into_tensor()
772 }
773
774 #[test]
775 fn cubic_resize_1d_upsample() {
776 let out = cubic_resize(tract_ndarray::arr1(&[0.0f32, 1.0, 2.0, 3.0]).into_tensor(), &[2.0]);
777 let plain = out.try_as_plain_ram().unwrap();
778 let output = plain.as_slice::<f32>().unwrap();
779 assert_eq!(output.len(), 8);
780 assert!((output[0] - (-0.10546875)).abs() < 1e-4, "got {}", output[0]);
781 }
782
783 #[test]
784 fn cubic_resize_2d_upsample() {
785 let out = cubic_resize(
786 tract_ndarray::arr2(&[[1.0f32, 2.0], [3.0, 4.0]]).into_tensor(),
787 &[2.0, 2.0],
788 );
789 assert_eq!(out.shape(), &[4, 4]);
790 }
791
792 fn replicates(coord_transformer: CoordTransformer, nearest: Nearest, scale: usize) -> bool {
793 let op = Resize {
794 coord_transformer,
795 interpolator: Interpolator::Nearest,
796 nearest,
797 optional_scales_input: Some(1),
798 optional_sizes_input: None,
799 };
800 is_pixel_replication(&op.plan_axis(scale as f32, 4, 4 * scale), scale)
801 }
802
803 #[test]
804 fn only_some_nearest_modes_replicate_pixels() {
805 assert!(replicates(CoordTransformer::Asymmetric, Nearest::Floor, 2));
806 assert!(replicates(CoordTransformer::HalfPixel, Nearest::RoundPreferCeil, 2));
807 assert!(replicates(CoordTransformer::HalfPixel, Nearest::RoundPreferCeil, 3));
808 assert!(!replicates(CoordTransformer::HalfPixel, Nearest::Floor, 2));
809 assert!(!replicates(CoordTransformer::Asymmetric, Nearest::RoundPreferCeil, 2));
810 }
811}