# Copyright (c) ONNX Project Contributors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import numbers import numpy as np from onnx.reference.op_run import OpRun from onnx.reference.ops.op_resize import _get_all_coords class GridSample(OpRun): # https://github.com/pytorch/pytorch/blob/v2.0.0/aten/src/ATen/native/GridSampler.h#L26 def _gs_denormalize(self, n, length: int, align_corners: bool): # type: ignore # n is the normalized coordinate (float) # x is the unormalized coordinate (float) if align_corners: # Align to corners # x_min = 0 # x_max = d-1 # Linear mapping from [x_min, x_max] to [-1, 1] # Solving linear equation n = ax + b # a = 2/(d-1) # b = -1 # n = 2/(d-1) x - 1 # n(d-1) = 2x - (d-1) # x = (n+1)(d-1) / 2 x = (n + 1) / 2.0 * (length - 1) else: # Not align to corners # x_min = -0.5 # x_max = d-0.5 # Linear mapping from [x_min, x_max] to [-1, 1] # Solving linear equation n = ax + b # a = 2/d # b = 1/d - 1 # n = 2/d x + 1/d - 1 # nd = 2x + 1 - d # x = (nd + d - 1) / 2 # x = ((n + 1) d - 1) / 2 x = ((n + 1) * length - 1) / 2.0 return x def _gs_denormalize_coordinates(self, n, dims, align_corners: bool): x = np.zeros(len(n), dtype=np.float32) for i, (v, dim) in enumerate(zip(n, dims)): x[i] = self._gs_denormalize(n=v, length=dim, align_corners=align_corners) return x def _gs_reflect(self, x, x_min, x_max): # type: ignore """Reflect by the near border till within the borders Use float for borders to avoid potential issues with integer T """ fx = x rng = x_max - x_min if fx < x_min: dx = x_min - fx n = int(dx / rng) r = dx - n * rng if n % 2 == 0: fx = x_min + r else: fx = x_max - r elif fx > x_max: dx = fx - x_max n = int(dx / rng) r = dx - n * rng if n % 2 == 0: fx = x_max - r else: fx = x_min + r return fx def _gs_get_cubic_coeffs(self, x, coeffs): # type: ignore """Calculate cubic convolution interpolation coefficients ROBERT G. KEYS https://ieeexplore.ieee.org/document/1163711 Use float to avoid potential issues with integer. """ cubic_alpha = -0.75 x = abs(x) coeffs[0] = ( (cubic_alpha * (x + 1) - 5 * cubic_alpha) * (x + 1) + 8 * cubic_alpha ) * (x + 1) - 4 * cubic_alpha coeffs[1] = ((cubic_alpha + 2) * x - (cubic_alpha + 3)) * x * x + 1 coeffs[2] = ((cubic_alpha + 2) * (1 - x) - (cubic_alpha + 3)) * (1 - x) * ( 1 - x ) + 1 coeffs[3] = ( (cubic_alpha * (2 - x) - 5 * cubic_alpha) * (2 - x) + 8 * cubic_alpha ) * (2 - x) - 4 * cubic_alpha def _gs_get_linear_coeffs(self, x, coeffs): x = abs(x) coeffs[0] = 1 - x coeffs[1] = x def _gs_bicubic_interpolate(self, p, x, y): # type: ignore v = np.empty((4,), dtype=p.dtype) coeffs = np.empty((4,), dtype=p.dtype) self._gs_get_cubic_coeffs(x, coeffs) for i in range(4): v[i] = coeffs @ p[i, :] self._gs_get_cubic_coeffs(y, coeffs) return coeffs @ v def _gs_cubic_interpolation_1d_with_x(self, data, x, border, padding_mode): v = np.empty((4,), dtype=data.dtype) coeffs = np.empty((4,), dtype=data.dtype) x_0 = int(np.floor(x)) x_1 = x_0 + 1 x_2 = x_0 + 2 x_minus_1 = x_0 - 1 self._gs_get_cubic_coeffs(x - x_0, coeffs) v[0] = self._pixel_at_array( array=data, i=x_minus_1, border=border, padding_mode=padding_mode ) v[1] = self._pixel_at_array( array=data, i=x_0, border=border, padding_mode=padding_mode ) v[2] = self._pixel_at_array( array=data, i=x_1, border=border, padding_mode=padding_mode ) v[3] = self._pixel_at_array( array=data, i=x_2, border=border, padding_mode=padding_mode ) return coeffs @ v def _gs_linear_interpolation_1d_with_x(self, data, x, border, padding_mode): v = np.empty((2,), dtype=data.dtype) coeffs = np.empty((2,), dtype=data.dtype) x_0 = int(np.floor(x)) x_1 = x_0 + 1 self._gs_get_linear_coeffs(x - x_0, coeffs) v[0] = self._pixel_at_array( array=data, i=x_0, border=border, padding_mode=padding_mode ) v[1] = self._pixel_at_array( array=data, i=x_1, border=border, padding_mode=padding_mode ) return coeffs @ v def _gs_linear_interpolation_nd_with_x(self, data, x, border, padding_mode): num_dims = data.ndim assert num_dims == len(x) == int(len(border) / 2) if num_dims == 1: return self._gs_linear_interpolation_1d_with_x( data=data, x=x[0], border=border, padding_mode=padding_mode ) res1d = [] for i in range(data.shape[0]): r = self._gs_linear_interpolation_nd_with_x( data=data[i], x=x[1:], border=list(border[1:num_dims]) + list(border[1 + num_dims : 2 * num_dims]), padding_mode=padding_mode, ) res1d.append(r) res1d = np.array(res1d) return self._gs_linear_interpolation_1d_with_x( data=res1d, x=x[0], border=[border[0], border[num_dims]], padding_mode=padding_mode, ) def _gs_cubic_interpolation_nd_with_x(self, data, x, border, padding_mode): num_dims = data.ndim assert num_dims == len(x) == int(len(border) / 2) if num_dims == 1: return self._gs_cubic_interpolation_1d_with_x( data=data, x=x[0], border=border, padding_mode=padding_mode ) res1d = [] for i in range(data.shape[0]): r = self._gs_cubic_interpolation_nd_with_x( data=data[i], x=x[1:], border=list(border[1:num_dims]) + list(border[1 + num_dims : 2 * num_dims]), padding_mode=padding_mode, ) res1d.append(r) res1d = np.array(res1d) return self._gs_cubic_interpolation_1d_with_x( data=res1d, x=x[0], border=[border[0], border[num_dims]], padding_mode=padding_mode, ) def _clamp(self, val, lo, hi): # type: ignore if val < lo: return lo if val > hi: return hi return val def _pixel_at_ndarray(self, ndarray, x: list, border, padding_mode): # type: ignore # boarder: [x_1_min, x_2_min, ..., x_1_max, x_2_max, ...] num_dims = ndarray.ndim assert num_dims == len(x) == int(len(border) / 2) if num_dims == 1: return self._pixel_at_array( array=ndarray, i=x[0], border=border, padding_mode=padding_mode ) i = x[0] d = ndarray.shape[0] if padding_mode == "zeros": if i >= 0 and i < d: ndarray = ndarray[i] else: # Trick i = 0 ndarray = np.zeros_like(ndarray[i]) elif padding_mode == "border": i = self._clamp(i, 0, d - 1) ndarray = ndarray[i] else: # padding_mode == "reflection" i = int(self._gs_reflect(i, border[0], border[num_dims])) ndarray = ndarray[i] return self._pixel_at_ndarray( ndarray=ndarray, x=x[1:], border=list(border[1:num_dims]) + list(border[1 + num_dims : 2 * num_dims]), padding_mode=padding_mode, ) def _pixel_at_array(self, array, i: int, border, padding_mode): # type: ignore assert array.ndim == 1 d = array.shape[0] if padding_mode == "zeros": if i >= 0 and i < d: pixel = array[i] else: pixel = 0 elif padding_mode == "border": i = self._clamp(i, 0, d - 1) pixel = array[i] else: # padding_mode == "reflection" i = int(self._gs_reflect(i, border[0], border[1])) pixel = array[i] return pixel def _prepare_border(self, dims, align_corners: bool): # boarder: [x_1_min, x_2_min, ..., x_1_max, x_2_max, ...] num_dims = len(dims) borders = np.zeros(num_dims * 2) for i in range(num_dims): # min borders[i] = -0.5 # max borders[i + num_dims] = dims[i] - 0.5 if align_corners: # min borders[i] = 0.0 # max borders[i + num_dims] = dims[i] - 1.0 return borders def _cpp_std_round(self, x): # https://en.cppreference.com/w/cpp/numeric/math/round def round_single_value(v): if v >= 0.0: return np.floor(v + 0.5) else: return np.ceil(v - 0.5) if isinstance(x, numbers.Number): return round_single_value(x) else: assert x.ndim == 1 x_rounded = np.zeros_like(x) for i in range(x.shape[0]): x_rounded[i] = round_single_value(x[i]) x_rounded = x_rounded.astype(np.int32) return x_rounded def _run(self, X, grid, mode=None, padding_mode=None, align_corners=None): # This implementation supports GridSample arbitrary dimensions. mode = mode or self.mode # type: ignore padding_mode = padding_mode or self.padding_mode # type: ignore align_corners = align_corners or self.align_corners # type: ignore x_dims = X.shape grid_dims = grid.shape N = x_dims[0] C = x_dims[1] y_dims = (N, C, *grid_dims[1:-1]) if np.prod(y_dims) == 0: return np.array([], dtype=X.dtype) Y = np.empty(y_dims, dtype=X.dtype) for n in range(N): grid_data = grid[n] for c in range(C): # Because the indices in the grid_data are always in the "reverse" dimensional order. # To interpolate for certain positions, we either have to transpose the X_data or # reverse the indices. # In this implementation, we took the latter approach. X_data = X[n, c] num_dims = len(x_dims[2:]) dims = x_dims[2:] # Prepare borders. border = self._prepare_border(dims, align_corners=align_corners) for ox in _get_all_coords(Y[n, c]): # normalized coordinates. nx = grid_data[tuple(ox)] nx = nx[::-1] # denormalized coordinates. x = self._gs_denormalize_coordinates( n=nx, dims=dims, align_corners=align_corners ) if mode == "nearest": # PyTorch round the index to nearest even. # https://github.com/pytorch/pytorch/pull/97000 x = np.rint(x) # https://github.com/pytorch/pytorch/blob/v2.0.0/aten/src/ATen/native/GridSampler.h#L142 for i, v in enumerate(x): x_min = border[i] x_max = border[i + num_dims] if v < x_min or v > x_max: if padding_mode == "border": x[i] = self._clamp(v, 0, dims[i] - 1) elif padding_mode == "reflection": x[i] = self._gs_reflect(v, x_min, x_max) if mode == "nearest": x = x.astype(np.int32) Y[n][c][tuple(ox)] = self._pixel_at_ndarray( ndarray=X_data, x=x, border=border, padding_mode=padding_mode, ) elif mode == "linear": Y[n][c][tuple(ox)] = self._gs_linear_interpolation_nd_with_x( data=X_data, x=x, border=border, padding_mode=padding_mode ) elif mode == "cubic": Y[n][c][tuple(ox)] = self._gs_cubic_interpolation_nd_with_x( data=X_data, x=x, border=border, padding_mode=padding_mode ) else: raise RuntimeError( "GridSample interpolation only supports nearest, linear, and cubic modes." ) return (Y.astype(X.dtype),)