|
| 1 | +from typing import Any, Callable, List, Optional, Text |
| 2 | + |
| 3 | +import numpy as np |
| 4 | + |
| 5 | +from .error import RunError |
| 6 | + |
| 7 | + |
| 8 | +class Resize: |
| 9 | + def __init__(self, opset_version, **kwargs): |
| 10 | + self.version = opset_version |
| 11 | + self.coordinate_transformation_mode = kwargs.get("coordinate_transformation_mode", "half_pixel") |
| 12 | + self.cubic_coeff_a = kwargs.get("cubic_coeff_a", -0.75) |
| 13 | + self.exclude_outside = kwargs.get("exclude_outside", 0) |
| 14 | + self.extrapolation_value = kwargs.get("extrapolation_value", 0.0) |
| 15 | + self.mode = kwargs.get("mode", "nearest") |
| 16 | + self.nearest_mode = kwargs.get("nearest_mode", "round_prefer_floor") |
| 17 | + |
| 18 | + def run(self, x, roi=None, scales=None, sizes=None): |
| 19 | + if self.version < 10: |
| 20 | + raise RunError("Resize", self.version) |
| 21 | + elif self.version == 10: |
| 22 | + if self.mode == "linear": |
| 23 | + # just guess |
| 24 | + self.coordinate_transformation_mode = "asymmetric" |
| 25 | + scales = roi |
| 26 | + roi = None |
| 27 | + |
| 28 | + assert sizes is not None or scales is not None |
| 29 | + if self.mode == "nearest": |
| 30 | + return [ |
| 31 | + interpolate_nd( |
| 32 | + x, |
| 33 | + lambda r: nearest_coeffs(r, mode=self.nearest_mode), |
| 34 | + output_size=sizes, |
| 35 | + scale_factors=scales, |
| 36 | + roi=roi, |
| 37 | + coordinate_transformation_mode=self.coordinate_transformation_mode, |
| 38 | + extrapolation_value=self.extrapolation_value, |
| 39 | + exclude_outside=self.exclude_outside, |
| 40 | + ).astype(x.dtype) |
| 41 | + ] |
| 42 | + elif self.mode == "linear": |
| 43 | + return [ |
| 44 | + interpolate_nd( |
| 45 | + x, |
| 46 | + linear_coeffs, |
| 47 | + output_size=sizes, |
| 48 | + scale_factors=scales, |
| 49 | + roi=roi, |
| 50 | + coordinate_transformation_mode=self.coordinate_transformation_mode, |
| 51 | + extrapolation_value=self.extrapolation_value, |
| 52 | + exclude_outside=self.exclude_outside, |
| 53 | + ).astype(x.dtype) |
| 54 | + ] |
| 55 | + elif self.mode == "cubic": |
| 56 | + return [ |
| 57 | + interpolate_nd( |
| 58 | + x, |
| 59 | + lambda r: cubic_coeffs(r, self.cubic_coeff_a), |
| 60 | + output_size=sizes, |
| 61 | + scale_factors=scales, |
| 62 | + roi=roi, |
| 63 | + coordinate_transformation_mode=self.coordinate_transformation_mode, |
| 64 | + extrapolation_value=self.extrapolation_value, |
| 65 | + exclude_outside=self.exclude_outside, |
| 66 | + ).astype(x.dtype) |
| 67 | + ] |
| 68 | + else: |
| 69 | + raise RunError("Resize", self.version) |
| 70 | + |
| 71 | + |
| 72 | +# The following code has been copied from |
| 73 | +# https://github.com/onnx/onnx/blob/6af1ed14b2f74eb6b5a52e12e2ebffa65a34001b/onnx/backend/test/case/node/resize.py#L16-L228 |
| 74 | +# Copyrights (c) ONNX Project Contributers |
| 75 | +# License: Apache-2.0 |
| 76 | +def cartesian(arrays, out=None): |
| 77 | + # type: (List[np.ndarray], np.ndarray) -> np.ndarray |
| 78 | + """ |
| 79 | + From https://stackoverflow.com/a/1235363 |
| 80 | + Generate a cartesian product of input arrays. |
| 81 | + Parameters |
| 82 | + ---------- |
| 83 | + arrays : list of array-like |
| 84 | + 1-D arrays to form the cartesian product of. |
| 85 | + out : ndarray |
| 86 | + Array to place the cartesian product in. |
| 87 | + Returns |
| 88 | + ------- |
| 89 | + out : ndarray |
| 90 | + 2-D array of shape (M, len(arrays)) containing cartesian products |
| 91 | + formed of input arrays. |
| 92 | + Examples |
| 93 | + -------- |
| 94 | + >>> cartesian(([1, 2, 3], [4, 5], [6, 7])) |
| 95 | + array([[1, 4, 6], |
| 96 | + [1, 4, 7], |
| 97 | + [1, 5, 6], |
| 98 | + [1, 5, 7], |
| 99 | + [2, 4, 6], |
| 100 | + [2, 4, 7], |
| 101 | + [2, 5, 6], |
| 102 | + [2, 5, 7], |
| 103 | + [3, 4, 6], |
| 104 | + [3, 4, 7], |
| 105 | + [3, 5, 6], |
| 106 | + [3, 5, 7]]) |
| 107 | + """ |
| 108 | + |
| 109 | + arrays = [np.asarray(x) for x in arrays] |
| 110 | + dtype = arrays[0].dtype |
| 111 | + |
| 112 | + n = np.prod([x.size for x in arrays]) |
| 113 | + if out is None: |
| 114 | + out = np.zeros([n, len(arrays)], dtype=dtype) |
| 115 | + |
| 116 | + m = n // arrays[0].size |
| 117 | + out[:, 0] = np.repeat(arrays[0], m) |
| 118 | + if arrays[1:]: |
| 119 | + cartesian(arrays[1:], out=out[0:m, 1:]) |
| 120 | + for j in range(1, arrays[0].size): |
| 121 | + out[j * m : (j + 1) * m, 1:] = out[0:m, 1:] |
| 122 | + return out |
| 123 | + |
| 124 | + |
| 125 | +def interpolate_1d_with_x( |
| 126 | + data, # type: np.ndarray |
| 127 | + scale_factor, # type: float |
| 128 | + x, # type: float |
| 129 | + get_coeffs, # type: Callable[[float], np.ndarray] |
| 130 | + roi=None, # type: np.ndarray |
| 131 | + extrapolation_value=0.0, # type: float |
| 132 | + coordinate_transformation_mode="half_pixel", # type: Text |
| 133 | + exclude_outside=False, # type: bool |
| 134 | +): # type: (...) -> np.ndarray |
| 135 | + def get_neighbor_idxes(x, n, limit): # type: (float, int, int) -> np.ndarray |
| 136 | + """ |
| 137 | + Return the n nearest indexes to x among [0, limit), prefer the indexes smaller than x. |
| 138 | + As a result, the ratio must be in (0, 1] |
| 139 | + Examples: |
| 140 | + get_neighbor_idxes(4, 2, 10) == [3, 4] |
| 141 | + get_neighbor_idxes(4, 3, 10) == [3, 4, 5] |
| 142 | + get_neighbor_idxes(4.4, 3, 10) == [3, 4, 5] |
| 143 | + get_neighbor_idxes(4.5, 3, 10) == [3, 4, 5] |
| 144 | + get_neighbor_idxes(4.6, 3, 10) == [4, 5, 6] |
| 145 | + get_neighbor_idxes(4.4, 1, 10) == [4] |
| 146 | + get_neighbor_idxes(4.6, 1, 10) == [5] |
| 147 | + :param x: |
| 148 | + :param n: the number of the wanted indexes |
| 149 | + :param limit: the maximum value of index |
| 150 | + :return: An np.array containing n nearest indexes in ascending order |
| 151 | + """ |
| 152 | + idxes = sorted(range(limit), key=lambda idx: (abs(x - idx), idx))[:n] |
| 153 | + idxes = sorted(idxes) |
| 154 | + return np.array(idxes) |
| 155 | + |
| 156 | + def get_neighbor(x, n, data): # type: (float, int, np.ndarray) -> np.ndarray |
| 157 | + """ |
| 158 | + Pad `data` in 'edge' mode, and get n nearest elements in the padded array and their indexes in the original |
| 159 | + array |
| 160 | + :param x: center index (in the unpadded coordinate system) of the found nearest elements. |
| 161 | + :param n: the number of neighbors. |
| 162 | + :param data: the array |
| 163 | + :return: A tuple containing the indexes of neighbor elements (the index can be smaller than 0 or higher than |
| 164 | + len(data)) and the value of these elements |
| 165 | + """ |
| 166 | + pad_width = np.ceil(n / 2).astype(int) |
| 167 | + padded = np.pad(data, pad_width, mode="edge") |
| 168 | + x += pad_width |
| 169 | + |
| 170 | + idxes = get_neighbor_idxes(x, n, len(padded)) |
| 171 | + ret = padded[idxes] |
| 172 | + return idxes - pad_width, ret |
| 173 | + |
| 174 | + input_width = len(data) |
| 175 | + output_width = scale_factor * input_width |
| 176 | + if coordinate_transformation_mode == "align_corners": |
| 177 | + if output_width == 1: |
| 178 | + x_ori = 0.0 |
| 179 | + else: |
| 180 | + x_ori = x * (input_width - 1) / (output_width - 1) |
| 181 | + elif coordinate_transformation_mode == "asymmetric": |
| 182 | + x_ori = x / scale_factor |
| 183 | + elif coordinate_transformation_mode == "tf_crop_and_resize": |
| 184 | + if output_width == 1: |
| 185 | + x_ori = (roi[1] - roi[0]) * (input_width - 1) / 2 |
| 186 | + else: |
| 187 | + x_ori = x * (roi[1] - roi[0]) * (input_width - 1) / (output_width - 1) |
| 188 | + x_ori += roi[0] * (input_width - 1) |
| 189 | + # Return extrapolation_value directly as what TF CropAndResize does |
| 190 | + if x_ori < 0 or x_ori > input_width - 1: |
| 191 | + return extrapolation_value |
| 192 | + elif coordinate_transformation_mode == "pytorch_half_pixel": |
| 193 | + if output_width == 1: |
| 194 | + x_ori = -0.5 |
| 195 | + else: |
| 196 | + x_ori = (x + 0.5) / scale_factor - 0.5 |
| 197 | + else: # coordinate_transformation_mode == 'half_pixel' |
| 198 | + x_ori = (x + 0.5) / scale_factor - 0.5 |
| 199 | + x_ori_int = np.floor(x_ori).astype(int).item() |
| 200 | + |
| 201 | + # ratio must be in (0, 1] since we prefer the pixel on the left of `x_ori` |
| 202 | + if x_ori.is_integer(): |
| 203 | + ratio = 1 |
| 204 | + else: |
| 205 | + ratio = x_ori - x_ori_int |
| 206 | + |
| 207 | + coeffs = get_coeffs(ratio) |
| 208 | + n = len(coeffs) |
| 209 | + |
| 210 | + idxes, points = get_neighbor(x_ori, n, data) |
| 211 | + |
| 212 | + if exclude_outside: |
| 213 | + for i, idx in enumerate(idxes): |
| 214 | + if idx < 0 or idx >= input_width: |
| 215 | + coeffs[i] = 0 |
| 216 | + coeffs /= sum(coeffs) |
| 217 | + |
| 218 | + return np.dot(coeffs, points).item() |
| 219 | + |
| 220 | + |
| 221 | +def interpolate_nd_with_x( |
| 222 | + data, # type: np.ndarray |
| 223 | + n, # type: int |
| 224 | + scale_factors, # type: List[float] |
| 225 | + x, # type: List[float] |
| 226 | + get_coeffs, # type: Callable[[float], np.ndarray] |
| 227 | + roi=None, # type: np.ndarray |
| 228 | + **kwargs, # type: Any |
| 229 | +): # type: (...) -> np.ndarray |
| 230 | + if n == 1: |
| 231 | + return interpolate_1d_with_x(data, scale_factors[0], x[0], get_coeffs, roi=roi, **kwargs) |
| 232 | + return interpolate_1d_with_x( |
| 233 | + [ |
| 234 | + interpolate_nd_with_x( |
| 235 | + data[i], |
| 236 | + n - 1, |
| 237 | + scale_factors[1:], |
| 238 | + x[1:], |
| 239 | + get_coeffs, |
| 240 | + roi=None if roi is None else np.concatenate([roi[1:n], roi[n + 1 :]]), |
| 241 | + **kwargs, |
| 242 | + ) |
| 243 | + for i in range(data.shape[0]) |
| 244 | + ], |
| 245 | + scale_factors[0], |
| 246 | + x[0], |
| 247 | + get_coeffs, |
| 248 | + roi=None if roi is None else [roi[0], roi[n]], |
| 249 | + **kwargs, |
| 250 | + ) |
| 251 | + |
| 252 | + |
| 253 | +def interpolate_nd( |
| 254 | + data, # type: np.ndarray |
| 255 | + get_coeffs, # type: Callable[[float], np.ndarray] |
| 256 | + output_size=None, # type: Optional[List[int]] |
| 257 | + scale_factors=None, # type: Optional[List[float]] |
| 258 | + roi=None, # type: np.ndarray |
| 259 | + **kwargs, # type: Any |
| 260 | +): # type: (...) -> np.ndarray |
| 261 | + def get_all_coords(data): # type: (np.ndarray) -> np.ndarray |
| 262 | + return cartesian([list(range(data.shape[i])) for i in range(len(data.shape))]) |
| 263 | + |
| 264 | + assert output_size is not None or scale_factors is not None |
| 265 | + if output_size is not None: |
| 266 | + scale_factors = np.array(output_size) / np.array(data.shape) |
| 267 | + else: |
| 268 | + output_size = (scale_factors * np.array(data.shape)).astype(int) |
| 269 | + assert scale_factors is not None |
| 270 | + |
| 271 | + ret = np.zeros(output_size) |
| 272 | + for x in get_all_coords(ret): |
| 273 | + ret[tuple(x)] = interpolate_nd_with_x(data, len(data.shape), scale_factors, x, get_coeffs, roi=roi, **kwargs) |
| 274 | + return ret |
| 275 | + |
| 276 | + |
| 277 | +def cubic_coeffs(ratio, A=-0.75): # type: (float, float) -> np.ndarray |
| 278 | + coeffs = [ |
| 279 | + ((A * (ratio + 1) - 5 * A) * (ratio + 1) + 8 * A) * (ratio + 1) - 4 * A, |
| 280 | + ((A + 2) * ratio - (A + 3)) * ratio * ratio + 1, |
| 281 | + ((A + 2) * (1 - ratio) - (A + 3)) * (1 - ratio) * (1 - ratio) + 1, |
| 282 | + ((A * ((1 - ratio) + 1) - 5 * A) * ((1 - ratio) + 1) + 8 * A) * ((1 - ratio) + 1) - 4 * A, |
| 283 | + ] |
| 284 | + |
| 285 | + return np.array(coeffs) |
| 286 | + |
| 287 | + |
| 288 | +def linear_coeffs(ratio): # type: (float) -> np.ndarray |
| 289 | + return np.array([1 - ratio, ratio]) |
| 290 | + |
| 291 | + |
| 292 | +def nearest_coeffs(ratio, mode="round_prefer_floor"): # type: (float, Text) -> np.ndarray |
| 293 | + if type(ratio) == int or ratio.is_integer(): |
| 294 | + return np.array([0, 1]) |
| 295 | + elif mode == "round_prefer_floor": |
| 296 | + return np.array([ratio <= 0.5, ratio > 0.5]) |
| 297 | + elif mode == "round_prefer_ceil": |
| 298 | + return np.array([ratio < 0.5, ratio >= 0.5]) |
| 299 | + elif mode == "floor": |
| 300 | + return np.array([1, 0]) |
| 301 | + elif mode == "ceil": |
| 302 | + return np.array([0, 1]) |
0 commit comments