image_transforms.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. # coding=utf-8
  2. # Copyright 2022 The HuggingFace Inc. team.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import warnings
  16. from math import ceil
  17. from typing import Iterable, List, Optional, Tuple, Union
  18. import numpy as np
  19. from .image_utils import (
  20. ChannelDimension,
  21. ImageInput,
  22. get_channel_dimension_axis,
  23. get_image_size,
  24. infer_channel_dimension_format,
  25. )
  26. from .utils import ExplicitEnum, TensorType, is_jax_tensor, is_tf_tensor, is_torch_tensor
  27. from .utils.import_utils import (
  28. is_flax_available,
  29. is_tf_available,
  30. is_torch_available,
  31. is_torchvision_available,
  32. is_torchvision_v2_available,
  33. is_vision_available,
  34. requires_backends,
  35. )
  36. if is_vision_available():
  37. import PIL
  38. from .image_utils import PILImageResampling
  39. if is_torch_available():
  40. import torch
  41. if is_tf_available():
  42. import tensorflow as tf
  43. if is_flax_available():
  44. import jax.numpy as jnp
  45. if is_torchvision_v2_available():
  46. from torchvision.transforms.v2 import functional as F
  47. elif is_torchvision_available():
  48. from torchvision.transforms import functional as F
  49. def to_channel_dimension_format(
  50. image: np.ndarray,
  51. channel_dim: Union[ChannelDimension, str],
  52. input_channel_dim: Optional[Union[ChannelDimension, str]] = None,
  53. ) -> np.ndarray:
  54. """
  55. Converts `image` to the channel dimension format specified by `channel_dim`.
  56. Args:
  57. image (`numpy.ndarray`):
  58. The image to have its channel dimension set.
  59. channel_dim (`ChannelDimension`):
  60. The channel dimension format to use.
  61. input_channel_dim (`ChannelDimension`, *optional*):
  62. The channel dimension format of the input image. If not provided, it will be inferred from the input image.
  63. Returns:
  64. `np.ndarray`: The image with the channel dimension set to `channel_dim`.
  65. """
  66. if not isinstance(image, np.ndarray):
  67. raise TypeError(f"Input image must be of type np.ndarray, got {type(image)}")
  68. if input_channel_dim is None:
  69. input_channel_dim = infer_channel_dimension_format(image)
  70. target_channel_dim = ChannelDimension(channel_dim)
  71. if input_channel_dim == target_channel_dim:
  72. return image
  73. if target_channel_dim == ChannelDimension.FIRST:
  74. image = image.transpose((2, 0, 1))
  75. elif target_channel_dim == ChannelDimension.LAST:
  76. image = image.transpose((1, 2, 0))
  77. else:
  78. raise ValueError("Unsupported channel dimension format: {}".format(channel_dim))
  79. return image
  80. def rescale(
  81. image: np.ndarray,
  82. scale: float,
  83. data_format: Optional[ChannelDimension] = None,
  84. dtype: np.dtype = np.float32,
  85. input_data_format: Optional[Union[str, ChannelDimension]] = None,
  86. ) -> np.ndarray:
  87. """
  88. Rescales `image` by `scale`.
  89. Args:
  90. image (`np.ndarray`):
  91. The image to rescale.
  92. scale (`float`):
  93. The scale to use for rescaling the image.
  94. data_format (`ChannelDimension`, *optional*):
  95. The channel dimension format of the image. If not provided, it will be the same as the input image.
  96. dtype (`np.dtype`, *optional*, defaults to `np.float32`):
  97. The dtype of the output image. Defaults to `np.float32`. Used for backwards compatibility with feature
  98. extractors.
  99. input_data_format (`ChannelDimension`, *optional*):
  100. The channel dimension format of the input image. If not provided, it will be inferred from the input image.
  101. Returns:
  102. `np.ndarray`: The rescaled image.
  103. """
  104. if not isinstance(image, np.ndarray):
  105. raise TypeError(f"Input image must be of type np.ndarray, got {type(image)}")
  106. rescaled_image = image.astype(np.float64) * scale # Numpy type promotion has changed, so always upcast first
  107. if data_format is not None:
  108. rescaled_image = to_channel_dimension_format(rescaled_image, data_format, input_data_format)
  109. rescaled_image = rescaled_image.astype(dtype) # Finally downcast to the desired dtype at the end
  110. return rescaled_image
  111. def _rescale_for_pil_conversion(image):
  112. """
  113. Detects whether or not the image needs to be rescaled before being converted to a PIL image.
  114. The assumption is that if the image is of type `np.float` and all values are between 0 and 1, it needs to be
  115. rescaled.
  116. """
  117. if image.dtype == np.uint8:
  118. do_rescale = False
  119. elif np.allclose(image, image.astype(int)):
  120. if np.all(0 <= image) and np.all(image <= 255):
  121. do_rescale = False
  122. else:
  123. raise ValueError(
  124. "The image to be converted to a PIL image contains values outside the range [0, 255], "
  125. f"got [{image.min()}, {image.max()}] which cannot be converted to uint8."
  126. )
  127. elif np.all(0 <= image) and np.all(image <= 1):
  128. do_rescale = True
  129. else:
  130. raise ValueError(
  131. "The image to be converted to a PIL image contains values outside the range [0, 1], "
  132. f"got [{image.min()}, {image.max()}] which cannot be converted to uint8."
  133. )
  134. return do_rescale
  135. def to_pil_image(
  136. image: Union[np.ndarray, "PIL.Image.Image", "torch.Tensor", "tf.Tensor", "jnp.ndarray"],
  137. do_rescale: Optional[bool] = None,
  138. image_mode: Optional[str] = None,
  139. input_data_format: Optional[Union[str, ChannelDimension]] = None,
  140. ) -> "PIL.Image.Image":
  141. """
  142. Converts `image` to a PIL Image. Optionally rescales it and puts the channel dimension back as the last axis if
  143. needed.
  144. Args:
  145. image (`PIL.Image.Image` or `numpy.ndarray` or `torch.Tensor` or `tf.Tensor`):
  146. The image to convert to the `PIL.Image` format.
  147. do_rescale (`bool`, *optional*):
  148. Whether or not to apply the scaling factor (to make pixel values integers between 0 and 255). Will default
  149. to `True` if the image type is a floating type and casting to `int` would result in a loss of precision,
  150. and `False` otherwise.
  151. image_mode (`str`, *optional*):
  152. The mode to use for the PIL image. If unset, will use the default mode for the input image type.
  153. input_data_format (`ChannelDimension`, *optional*):
  154. The channel dimension format of the input image. If unset, will use the inferred format from the input.
  155. Returns:
  156. `PIL.Image.Image`: The converted image.
  157. """
  158. requires_backends(to_pil_image, ["vision"])
  159. if isinstance(image, PIL.Image.Image):
  160. return image
  161. # Convert all tensors to numpy arrays before converting to PIL image
  162. if is_torch_tensor(image) or is_tf_tensor(image):
  163. image = image.numpy()
  164. elif is_jax_tensor(image):
  165. image = np.array(image)
  166. elif not isinstance(image, np.ndarray):
  167. raise ValueError("Input image type not supported: {}".format(type(image)))
  168. # If the channel has been moved to first dim, we put it back at the end.
  169. image = to_channel_dimension_format(image, ChannelDimension.LAST, input_data_format)
  170. # If there is a single channel, we squeeze it, as otherwise PIL can't handle it.
  171. image = np.squeeze(image, axis=-1) if image.shape[-1] == 1 else image
  172. # PIL.Image can only store uint8 values so we rescale the image to be between 0 and 255 if needed.
  173. do_rescale = _rescale_for_pil_conversion(image) if do_rescale is None else do_rescale
  174. if do_rescale:
  175. image = rescale(image, 255)
  176. image = image.astype(np.uint8)
  177. return PIL.Image.fromarray(image, mode=image_mode)
  178. # Logic adapted from torchvision resizing logic: https://github.com/pytorch/vision/blob/511924c1ced4ce0461197e5caa64ce5b9e558aab/torchvision/transforms/functional.py#L366
  179. def get_resize_output_image_size(
  180. input_image: np.ndarray,
  181. size: Union[int, Tuple[int, int], List[int], Tuple[int]],
  182. default_to_square: bool = True,
  183. max_size: Optional[int] = None,
  184. input_data_format: Optional[Union[str, ChannelDimension]] = None,
  185. ) -> tuple:
  186. """
  187. Find the target (height, width) dimension of the output image after resizing given the input image and the desired
  188. size.
  189. Args:
  190. input_image (`np.ndarray`):
  191. The image to resize.
  192. size (`int` or `Tuple[int, int]` or List[int] or `Tuple[int]`):
  193. The size to use for resizing the image. If `size` is a sequence like (h, w), output size will be matched to
  194. this.
  195. If `size` is an int and `default_to_square` is `True`, then image will be resized to (size, size). If
  196. `size` is an int and `default_to_square` is `False`, then smaller edge of the image will be matched to this
  197. number. i.e, if height > width, then image will be rescaled to (size * height / width, size).
  198. default_to_square (`bool`, *optional*, defaults to `True`):
  199. How to convert `size` when it is a single int. If set to `True`, the `size` will be converted to a square
  200. (`size`,`size`). If set to `False`, will replicate
  201. [`torchvision.transforms.Resize`](https://pytorch.org/vision/stable/transforms.html#torchvision.transforms.Resize)
  202. with support for resizing only the smallest edge and providing an optional `max_size`.
  203. max_size (`int`, *optional*):
  204. The maximum allowed for the longer edge of the resized image: if the longer edge of the image is greater
  205. than `max_size` after being resized according to `size`, then the image is resized again so that the longer
  206. edge is equal to `max_size`. As a result, `size` might be overruled, i.e the smaller edge may be shorter
  207. than `size`. Only used if `default_to_square` is `False`.
  208. input_data_format (`ChannelDimension`, *optional*):
  209. The channel dimension format of the input image. If unset, will use the inferred format from the input.
  210. Returns:
  211. `tuple`: The target (height, width) dimension of the output image after resizing.
  212. """
  213. if isinstance(size, (tuple, list)):
  214. if len(size) == 2:
  215. return tuple(size)
  216. elif len(size) == 1:
  217. # Perform same logic as if size was an int
  218. size = size[0]
  219. else:
  220. raise ValueError("size must have 1 or 2 elements if it is a list or tuple")
  221. if default_to_square:
  222. return (size, size)
  223. height, width = get_image_size(input_image, input_data_format)
  224. short, long = (width, height) if width <= height else (height, width)
  225. requested_new_short = size
  226. new_short, new_long = requested_new_short, int(requested_new_short * long / short)
  227. if max_size is not None:
  228. if max_size <= requested_new_short:
  229. raise ValueError(
  230. f"max_size = {max_size} must be strictly greater than the requested "
  231. f"size for the smaller edge size = {size}"
  232. )
  233. if new_long > max_size:
  234. new_short, new_long = int(max_size * new_short / new_long), max_size
  235. return (new_long, new_short) if width <= height else (new_short, new_long)
  236. def resize(
  237. image: np.ndarray,
  238. size: Tuple[int, int],
  239. resample: "PILImageResampling" = None,
  240. reducing_gap: Optional[int] = None,
  241. data_format: Optional[ChannelDimension] = None,
  242. return_numpy: bool = True,
  243. input_data_format: Optional[Union[str, ChannelDimension]] = None,
  244. ) -> np.ndarray:
  245. """
  246. Resizes `image` to `(height, width)` specified by `size` using the PIL library.
  247. Args:
  248. image (`np.ndarray`):
  249. The image to resize.
  250. size (`Tuple[int, int]`):
  251. The size to use for resizing the image.
  252. resample (`int`, *optional*, defaults to `PILImageResampling.BILINEAR`):
  253. The filter to user for resampling.
  254. reducing_gap (`int`, *optional*):
  255. Apply optimization by resizing the image in two steps. The bigger `reducing_gap`, the closer the result to
  256. the fair resampling. See corresponding Pillow documentation for more details.
  257. data_format (`ChannelDimension`, *optional*):
  258. The channel dimension format of the output image. If unset, will use the inferred format from the input.
  259. return_numpy (`bool`, *optional*, defaults to `True`):
  260. Whether or not to return the resized image as a numpy array. If False a `PIL.Image.Image` object is
  261. returned.
  262. input_data_format (`ChannelDimension`, *optional*):
  263. The channel dimension format of the input image. If unset, will use the inferred format from the input.
  264. Returns:
  265. `np.ndarray`: The resized image.
  266. """
  267. requires_backends(resize, ["vision"])
  268. resample = resample if resample is not None else PILImageResampling.BILINEAR
  269. if not len(size) == 2:
  270. raise ValueError("size must have 2 elements")
  271. # For all transformations, we want to keep the same data format as the input image unless otherwise specified.
  272. # The resized image from PIL will always have channels last, so find the input format first.
  273. if input_data_format is None:
  274. input_data_format = infer_channel_dimension_format(image)
  275. data_format = input_data_format if data_format is None else data_format
  276. # To maintain backwards compatibility with the resizing done in previous image feature extractors, we use
  277. # the pillow library to resize the image and then convert back to numpy
  278. do_rescale = False
  279. if not isinstance(image, PIL.Image.Image):
  280. do_rescale = _rescale_for_pil_conversion(image)
  281. image = to_pil_image(image, do_rescale=do_rescale, input_data_format=input_data_format)
  282. height, width = size
  283. # PIL images are in the format (width, height)
  284. resized_image = image.resize((width, height), resample=resample, reducing_gap=reducing_gap)
  285. if return_numpy:
  286. resized_image = np.array(resized_image)
  287. # If the input image channel dimension was of size 1, then it is dropped when converting to a PIL image
  288. # so we need to add it back if necessary.
  289. resized_image = np.expand_dims(resized_image, axis=-1) if resized_image.ndim == 2 else resized_image
  290. # The image is always in channels last format after converting from a PIL image
  291. resized_image = to_channel_dimension_format(
  292. resized_image, data_format, input_channel_dim=ChannelDimension.LAST
  293. )
  294. # If an image was rescaled to be in the range [0, 255] before converting to a PIL image, then we need to
  295. # rescale it back to the original range.
  296. resized_image = rescale(resized_image, 1 / 255) if do_rescale else resized_image
  297. return resized_image
  298. def normalize(
  299. image: np.ndarray,
  300. mean: Union[float, Iterable[float]],
  301. std: Union[float, Iterable[float]],
  302. data_format: Optional[ChannelDimension] = None,
  303. input_data_format: Optional[Union[str, ChannelDimension]] = None,
  304. ) -> np.ndarray:
  305. """
  306. Normalizes `image` using the mean and standard deviation specified by `mean` and `std`.
  307. image = (image - mean) / std
  308. Args:
  309. image (`np.ndarray`):
  310. The image to normalize.
  311. mean (`float` or `Iterable[float]`):
  312. The mean to use for normalization.
  313. std (`float` or `Iterable[float]`):
  314. The standard deviation to use for normalization.
  315. data_format (`ChannelDimension`, *optional*):
  316. The channel dimension format of the output image. If unset, will use the inferred format from the input.
  317. input_data_format (`ChannelDimension`, *optional*):
  318. The channel dimension format of the input image. If unset, will use the inferred format from the input.
  319. """
  320. if not isinstance(image, np.ndarray):
  321. raise ValueError("image must be a numpy array")
  322. if input_data_format is None:
  323. input_data_format = infer_channel_dimension_format(image)
  324. channel_axis = get_channel_dimension_axis(image, input_data_format=input_data_format)
  325. num_channels = image.shape[channel_axis]
  326. # We cast to float32 to avoid errors that can occur when subtracting uint8 values.
  327. # We preserve the original dtype if it is a float type to prevent upcasting float16.
  328. if not np.issubdtype(image.dtype, np.floating):
  329. image = image.astype(np.float32)
  330. if isinstance(mean, Iterable):
  331. if len(mean) != num_channels:
  332. raise ValueError(f"mean must have {num_channels} elements if it is an iterable, got {len(mean)}")
  333. else:
  334. mean = [mean] * num_channels
  335. mean = np.array(mean, dtype=image.dtype)
  336. if isinstance(std, Iterable):
  337. if len(std) != num_channels:
  338. raise ValueError(f"std must have {num_channels} elements if it is an iterable, got {len(std)}")
  339. else:
  340. std = [std] * num_channels
  341. std = np.array(std, dtype=image.dtype)
  342. if input_data_format == ChannelDimension.LAST:
  343. image = (image - mean) / std
  344. else:
  345. image = ((image.T - mean) / std).T
  346. image = to_channel_dimension_format(image, data_format, input_data_format) if data_format is not None else image
  347. return image
  348. def center_crop(
  349. image: np.ndarray,
  350. size: Tuple[int, int],
  351. data_format: Optional[Union[str, ChannelDimension]] = None,
  352. input_data_format: Optional[Union[str, ChannelDimension]] = None,
  353. return_numpy: Optional[bool] = None,
  354. ) -> np.ndarray:
  355. """
  356. Crops the `image` to the specified `size` using a center crop. Note that if the image is too small to be cropped to
  357. the size given, it will be padded (so the returned result will always be of size `size`).
  358. Args:
  359. image (`np.ndarray`):
  360. The image to crop.
  361. size (`Tuple[int, int]`):
  362. The target size for the cropped image.
  363. data_format (`str` or `ChannelDimension`, *optional*):
  364. The channel dimension format for the output image. Can be one of:
  365. - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
  366. - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
  367. If unset, will use the inferred format of the input image.
  368. input_data_format (`str` or `ChannelDimension`, *optional*):
  369. The channel dimension format for the input image. Can be one of:
  370. - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
  371. - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
  372. If unset, will use the inferred format of the input image.
  373. return_numpy (`bool`, *optional*):
  374. Whether or not to return the cropped image as a numpy array. Used for backwards compatibility with the
  375. previous ImageFeatureExtractionMixin method.
  376. - Unset: will return the same type as the input image.
  377. - `True`: will return a numpy array.
  378. - `False`: will return a `PIL.Image.Image` object.
  379. Returns:
  380. `np.ndarray`: The cropped image.
  381. """
  382. requires_backends(center_crop, ["vision"])
  383. if return_numpy is not None:
  384. warnings.warn("return_numpy is deprecated and will be removed in v.4.33", FutureWarning)
  385. return_numpy = True if return_numpy is None else return_numpy
  386. if not isinstance(image, np.ndarray):
  387. raise TypeError(f"Input image must be of type np.ndarray, got {type(image)}")
  388. if not isinstance(size, Iterable) or len(size) != 2:
  389. raise ValueError("size must have 2 elements representing the height and width of the output image")
  390. if input_data_format is None:
  391. input_data_format = infer_channel_dimension_format(image)
  392. output_data_format = data_format if data_format is not None else input_data_format
  393. # We perform the crop in (C, H, W) format and then convert to the output format
  394. image = to_channel_dimension_format(image, ChannelDimension.FIRST, input_data_format)
  395. orig_height, orig_width = get_image_size(image, ChannelDimension.FIRST)
  396. crop_height, crop_width = size
  397. crop_height, crop_width = int(crop_height), int(crop_width)
  398. # In case size is odd, (image_shape[0] + size[0]) // 2 won't give the proper result.
  399. top = (orig_height - crop_height) // 2
  400. bottom = top + crop_height
  401. # In case size is odd, (image_shape[1] + size[1]) // 2 won't give the proper result.
  402. left = (orig_width - crop_width) // 2
  403. right = left + crop_width
  404. # Check if cropped area is within image boundaries
  405. if top >= 0 and bottom <= orig_height and left >= 0 and right <= orig_width:
  406. image = image[..., top:bottom, left:right]
  407. image = to_channel_dimension_format(image, output_data_format, ChannelDimension.FIRST)
  408. return image
  409. # Otherwise, we may need to pad if the image is too small. Oh joy...
  410. new_height = max(crop_height, orig_height)
  411. new_width = max(crop_width, orig_width)
  412. new_shape = image.shape[:-2] + (new_height, new_width)
  413. new_image = np.zeros_like(image, shape=new_shape)
  414. # If the image is too small, pad it with zeros
  415. top_pad = ceil((new_height - orig_height) / 2)
  416. bottom_pad = top_pad + orig_height
  417. left_pad = ceil((new_width - orig_width) / 2)
  418. right_pad = left_pad + orig_width
  419. new_image[..., top_pad:bottom_pad, left_pad:right_pad] = image
  420. top += top_pad
  421. bottom += top_pad
  422. left += left_pad
  423. right += left_pad
  424. new_image = new_image[..., max(0, top) : min(new_height, bottom), max(0, left) : min(new_width, right)]
  425. new_image = to_channel_dimension_format(new_image, output_data_format, ChannelDimension.FIRST)
  426. if not return_numpy:
  427. new_image = to_pil_image(new_image)
  428. return new_image
  429. def _center_to_corners_format_torch(bboxes_center: "torch.Tensor") -> "torch.Tensor":
  430. center_x, center_y, width, height = bboxes_center.unbind(-1)
  431. bbox_corners = torch.stack(
  432. # top left x, top left y, bottom right x, bottom right y
  433. [(center_x - 0.5 * width), (center_y - 0.5 * height), (center_x + 0.5 * width), (center_y + 0.5 * height)],
  434. dim=-1,
  435. )
  436. return bbox_corners
  437. def _center_to_corners_format_numpy(bboxes_center: np.ndarray) -> np.ndarray:
  438. center_x, center_y, width, height = bboxes_center.T
  439. bboxes_corners = np.stack(
  440. # top left x, top left y, bottom right x, bottom right y
  441. [center_x - 0.5 * width, center_y - 0.5 * height, center_x + 0.5 * width, center_y + 0.5 * height],
  442. axis=-1,
  443. )
  444. return bboxes_corners
  445. def _center_to_corners_format_tf(bboxes_center: "tf.Tensor") -> "tf.Tensor":
  446. center_x, center_y, width, height = tf.unstack(bboxes_center, axis=-1)
  447. bboxes_corners = tf.stack(
  448. # top left x, top left y, bottom right x, bottom right y
  449. [center_x - 0.5 * width, center_y - 0.5 * height, center_x + 0.5 * width, center_y + 0.5 * height],
  450. axis=-1,
  451. )
  452. return bboxes_corners
  453. # 2 functions below inspired by https://github.com/facebookresearch/detr/blob/master/util/box_ops.py
  454. def center_to_corners_format(bboxes_center: TensorType) -> TensorType:
  455. """
  456. Converts bounding boxes from center format to corners format.
  457. center format: contains the coordinate for the center of the box and its width, height dimensions
  458. (center_x, center_y, width, height)
  459. corners format: contains the coodinates for the top-left and bottom-right corners of the box
  460. (top_left_x, top_left_y, bottom_right_x, bottom_right_y)
  461. """
  462. # Function is used during model forward pass, so we use the input framework if possible, without
  463. # converting to numpy
  464. if is_torch_tensor(bboxes_center):
  465. return _center_to_corners_format_torch(bboxes_center)
  466. elif isinstance(bboxes_center, np.ndarray):
  467. return _center_to_corners_format_numpy(bboxes_center)
  468. elif is_tf_tensor(bboxes_center):
  469. return _center_to_corners_format_tf(bboxes_center)
  470. raise ValueError(f"Unsupported input type {type(bboxes_center)}")
  471. def _corners_to_center_format_torch(bboxes_corners: "torch.Tensor") -> "torch.Tensor":
  472. top_left_x, top_left_y, bottom_right_x, bottom_right_y = bboxes_corners.unbind(-1)
  473. b = [
  474. (top_left_x + bottom_right_x) / 2, # center x
  475. (top_left_y + bottom_right_y) / 2, # center y
  476. (bottom_right_x - top_left_x), # width
  477. (bottom_right_y - top_left_y), # height
  478. ]
  479. return torch.stack(b, dim=-1)
  480. def _corners_to_center_format_numpy(bboxes_corners: np.ndarray) -> np.ndarray:
  481. top_left_x, top_left_y, bottom_right_x, bottom_right_y = bboxes_corners.T
  482. bboxes_center = np.stack(
  483. [
  484. (top_left_x + bottom_right_x) / 2, # center x
  485. (top_left_y + bottom_right_y) / 2, # center y
  486. (bottom_right_x - top_left_x), # width
  487. (bottom_right_y - top_left_y), # height
  488. ],
  489. axis=-1,
  490. )
  491. return bboxes_center
  492. def _corners_to_center_format_tf(bboxes_corners: "tf.Tensor") -> "tf.Tensor":
  493. top_left_x, top_left_y, bottom_right_x, bottom_right_y = tf.unstack(bboxes_corners, axis=-1)
  494. bboxes_center = tf.stack(
  495. [
  496. (top_left_x + bottom_right_x) / 2, # center x
  497. (top_left_y + bottom_right_y) / 2, # center y
  498. (bottom_right_x - top_left_x), # width
  499. (bottom_right_y - top_left_y), # height
  500. ],
  501. axis=-1,
  502. )
  503. return bboxes_center
  504. def corners_to_center_format(bboxes_corners: TensorType) -> TensorType:
  505. """
  506. Converts bounding boxes from corners format to center format.
  507. corners format: contains the coordinates for the top-left and bottom-right corners of the box
  508. (top_left_x, top_left_y, bottom_right_x, bottom_right_y)
  509. center format: contains the coordinate for the center of the box and its the width, height dimensions
  510. (center_x, center_y, width, height)
  511. """
  512. # Inverse function accepts different input types so implemented here too
  513. if is_torch_tensor(bboxes_corners):
  514. return _corners_to_center_format_torch(bboxes_corners)
  515. elif isinstance(bboxes_corners, np.ndarray):
  516. return _corners_to_center_format_numpy(bboxes_corners)
  517. elif is_tf_tensor(bboxes_corners):
  518. return _corners_to_center_format_tf(bboxes_corners)
  519. raise ValueError(f"Unsupported input type {type(bboxes_corners)}")
  520. # 2 functions below copied from https://github.com/cocodataset/panopticapi/blob/master/panopticapi/utils.py
  521. # Copyright (c) 2018, Alexander Kirillov
  522. # All rights reserved.
  523. def rgb_to_id(color):
  524. """
  525. Converts RGB color to unique ID.
  526. """
  527. if isinstance(color, np.ndarray) and len(color.shape) == 3:
  528. if color.dtype == np.uint8:
  529. color = color.astype(np.int32)
  530. return color[:, :, 0] + 256 * color[:, :, 1] + 256 * 256 * color[:, :, 2]
  531. return int(color[0] + 256 * color[1] + 256 * 256 * color[2])
  532. def id_to_rgb(id_map):
  533. """
  534. Converts unique ID to RGB color.
  535. """
  536. if isinstance(id_map, np.ndarray):
  537. id_map_copy = id_map.copy()
  538. rgb_shape = tuple(list(id_map.shape) + [3])
  539. rgb_map = np.zeros(rgb_shape, dtype=np.uint8)
  540. for i in range(3):
  541. rgb_map[..., i] = id_map_copy % 256
  542. id_map_copy //= 256
  543. return rgb_map
  544. color = []
  545. for _ in range(3):
  546. color.append(id_map % 256)
  547. id_map //= 256
  548. return color
  549. class PaddingMode(ExplicitEnum):
  550. """
  551. Enum class for the different padding modes to use when padding images.
  552. """
  553. CONSTANT = "constant"
  554. REFLECT = "reflect"
  555. REPLICATE = "replicate"
  556. SYMMETRIC = "symmetric"
  557. def pad(
  558. image: np.ndarray,
  559. padding: Union[int, Tuple[int, int], Iterable[Tuple[int, int]]],
  560. mode: PaddingMode = PaddingMode.CONSTANT,
  561. constant_values: Union[float, Iterable[float]] = 0.0,
  562. data_format: Optional[Union[str, ChannelDimension]] = None,
  563. input_data_format: Optional[Union[str, ChannelDimension]] = None,
  564. ) -> np.ndarray:
  565. """
  566. Pads the `image` with the specified (height, width) `padding` and `mode`.
  567. Args:
  568. image (`np.ndarray`):
  569. The image to pad.
  570. padding (`int` or `Tuple[int, int]` or `Iterable[Tuple[int, int]]`):
  571. Padding to apply to the edges of the height, width axes. Can be one of three formats:
  572. - `((before_height, after_height), (before_width, after_width))` unique pad widths for each axis.
  573. - `((before, after),)` yields same before and after pad for height and width.
  574. - `(pad,)` or int is a shortcut for before = after = pad width for all axes.
  575. mode (`PaddingMode`):
  576. The padding mode to use. Can be one of:
  577. - `"constant"`: pads with a constant value.
  578. - `"reflect"`: pads with the reflection of the vector mirrored on the first and last values of the
  579. vector along each axis.
  580. - `"replicate"`: pads with the replication of the last value on the edge of the array along each axis.
  581. - `"symmetric"`: pads with the reflection of the vector mirrored along the edge of the array.
  582. constant_values (`float` or `Iterable[float]`, *optional*):
  583. The value to use for the padding if `mode` is `"constant"`.
  584. data_format (`str` or `ChannelDimension`, *optional*):
  585. The channel dimension format for the output image. Can be one of:
  586. - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
  587. - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
  588. If unset, will use same as the input image.
  589. input_data_format (`str` or `ChannelDimension`, *optional*):
  590. The channel dimension format for the input image. Can be one of:
  591. - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
  592. - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
  593. If unset, will use the inferred format of the input image.
  594. Returns:
  595. `np.ndarray`: The padded image.
  596. """
  597. if input_data_format is None:
  598. input_data_format = infer_channel_dimension_format(image)
  599. def _expand_for_data_format(values):
  600. """
  601. Convert values to be in the format expected by np.pad based on the data format.
  602. """
  603. if isinstance(values, (int, float)):
  604. values = ((values, values), (values, values))
  605. elif isinstance(values, tuple) and len(values) == 1:
  606. values = ((values[0], values[0]), (values[0], values[0]))
  607. elif isinstance(values, tuple) and len(values) == 2 and isinstance(values[0], int):
  608. values = (values, values)
  609. elif isinstance(values, tuple) and len(values) == 2 and isinstance(values[0], tuple):
  610. values = values
  611. else:
  612. raise ValueError(f"Unsupported format: {values}")
  613. # add 0 for channel dimension
  614. values = ((0, 0), *values) if input_data_format == ChannelDimension.FIRST else (*values, (0, 0))
  615. # Add additional padding if there's a batch dimension
  616. values = (0, *values) if image.ndim == 4 else values
  617. return values
  618. padding = _expand_for_data_format(padding)
  619. if mode == PaddingMode.CONSTANT:
  620. constant_values = _expand_for_data_format(constant_values)
  621. image = np.pad(image, padding, mode="constant", constant_values=constant_values)
  622. elif mode == PaddingMode.REFLECT:
  623. image = np.pad(image, padding, mode="reflect")
  624. elif mode == PaddingMode.REPLICATE:
  625. image = np.pad(image, padding, mode="edge")
  626. elif mode == PaddingMode.SYMMETRIC:
  627. image = np.pad(image, padding, mode="symmetric")
  628. else:
  629. raise ValueError(f"Invalid padding mode: {mode}")
  630. image = to_channel_dimension_format(image, data_format, input_data_format) if data_format is not None else image
  631. return image
  632. # TODO (Amy): Accept 1/3/4 channel numpy array as input and return np.array as default
  633. def convert_to_rgb(image: ImageInput) -> ImageInput:
  634. """
  635. Converts an image to RGB format. Only converts if the image is of type PIL.Image.Image, otherwise returns the image
  636. as is.
  637. Args:
  638. image (Image):
  639. The image to convert.
  640. """
  641. requires_backends(convert_to_rgb, ["vision"])
  642. if not isinstance(image, PIL.Image.Image):
  643. return image
  644. if image.mode == "RGB":
  645. return image
  646. image = image.convert("RGB")
  647. return image
  648. def flip_channel_order(
  649. image: np.ndarray,
  650. data_format: Optional[ChannelDimension] = None,
  651. input_data_format: Optional[Union[str, ChannelDimension]] = None,
  652. ) -> np.ndarray:
  653. """
  654. Flips the channel order of the image.
  655. If the image is in RGB format, it will be converted to BGR and vice versa.
  656. Args:
  657. image (`np.ndarray`):
  658. The image to flip.
  659. data_format (`ChannelDimension`, *optional*):
  660. The channel dimension format for the output image. Can be one of:
  661. - `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
  662. - `ChannelDimension.LAST`: image in (height, width, num_channels) format.
  663. If unset, will use same as the input image.
  664. input_data_format (`ChannelDimension`, *optional*):
  665. The channel dimension format for the input image. Can be one of:
  666. - `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
  667. - `ChannelDimension.LAST`: image in (height, width, num_channels) format.
  668. If unset, will use the inferred format of the input image.
  669. """
  670. input_data_format = infer_channel_dimension_format(image) if input_data_format is None else input_data_format
  671. if input_data_format == ChannelDimension.LAST:
  672. image = image[..., ::-1]
  673. elif input_data_format == ChannelDimension.FIRST:
  674. image = image[::-1, ...]
  675. else:
  676. raise ValueError(f"Unsupported channel dimension: {input_data_format}")
  677. if data_format is not None:
  678. image = to_channel_dimension_format(image, data_format, input_channel_dim=input_data_format)
  679. return image
  680. def _cast_tensor_to_float(x):
  681. if x.is_floating_point():
  682. return x
  683. return x.float()
  684. class FusedRescaleNormalize:
  685. """
  686. Rescale and normalize the input image in one step.
  687. """
  688. def __init__(self, mean, std, rescale_factor: float = 1.0, inplace: bool = False):
  689. self.mean = torch.tensor(mean) * (1.0 / rescale_factor)
  690. self.std = torch.tensor(std) * (1.0 / rescale_factor)
  691. self.inplace = inplace
  692. def __call__(self, image: "torch.Tensor"):
  693. image = _cast_tensor_to_float(image)
  694. return F.normalize(image, self.mean, self.std, inplace=self.inplace)
  695. class Rescale:
  696. """
  697. Rescale the input image by rescale factor: image *= rescale_factor.
  698. """
  699. def __init__(self, rescale_factor: float = 1.0):
  700. self.rescale_factor = rescale_factor
  701. def __call__(self, image: "torch.Tensor"):
  702. image = image * self.rescale_factor
  703. return image
  704. class NumpyToTensor:
  705. """
  706. Convert a numpy array to a PyTorch tensor.
  707. """
  708. def __call__(self, image: np.ndarray):
  709. # Same as in PyTorch, we assume incoming numpy images are in HWC format
  710. # c.f. https://github.com/pytorch/vision/blob/61d97f41bc209e1407dcfbd685d2ee2da9c1cdad/torchvision/transforms/functional.py#L154
  711. return torch.from_numpy(image.transpose(2, 0, 1)).contiguous()