heatmaps.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. """Classes to represent heatmaps, i.e. float arrays of ``[0.0, 1.0]``."""
  2. from __future__ import print_function, division, absolute_import
  3. import numpy as np
  4. import six.moves as sm
  5. from .. import imgaug as ia
  6. from .base import IAugmentable
  7. class HeatmapsOnImage(IAugmentable):
  8. """Object representing heatmaps on a single image.
  9. Parameters
  10. ----------
  11. arr : (H,W) ndarray or (H,W,C) ndarray
  12. Array representing the heatmap(s) on a single image.
  13. Multiple heatmaps may be provided, in which case ``C`` is expected to
  14. denote the heatmap index.
  15. The array must be of dtype ``float32``.
  16. shape : tuple of int
  17. Shape of the image on which the heatmap(s) is/are placed.
  18. **Not** the shape of the heatmap(s) array, unless it is identical
  19. to the image shape (note the likely difference between the arrays
  20. in the number of channels).
  21. This is expected to be ``(H, W)`` or ``(H, W, C)`` with ``C`` usually
  22. being ``3``.
  23. If there is no corresponding image, use ``(H_arr, W_arr)`` instead,
  24. where ``H_arr`` is the height of the heatmap(s) array
  25. (analogous ``W_arr``).
  26. min_value : float, optional
  27. Minimum value for the heatmaps that `arr` represents. This will
  28. usually be ``0.0``.
  29. max_value : float, optional
  30. Maximum value for the heatmaps that `arr` represents. This will
  31. usually be ``1.0``.
  32. """
  33. def __init__(self, arr, shape, min_value=0.0, max_value=1.0):
  34. """Construct a new HeatmapsOnImage object."""
  35. assert ia.is_np_array(arr), (
  36. "Expected numpy array as heatmap input array, "
  37. "got type %s" % (type(arr),))
  38. # TODO maybe allow 0-sized heatmaps? in that case the min() and max()
  39. # must be adjusted
  40. assert arr.shape[0] > 0 and arr.shape[1] > 0, (
  41. "Expected numpy array as heatmap with height and width greater "
  42. "than 0, got shape %s." % (arr.shape,))
  43. assert arr.dtype.name in ["float32"], (
  44. "Heatmap input array expected to be of dtype float32, "
  45. "got dtype %s." % (arr.dtype,))
  46. assert arr.ndim in [2, 3], (
  47. "Heatmap input array must be 2d or 3d, got shape %s." % (
  48. arr.shape,))
  49. assert len(shape) in [2, 3], (
  50. "Argument 'shape' in HeatmapsOnImage expected to be 2d or 3d, "
  51. "got shape %s." % (shape,))
  52. assert min_value < max_value, (
  53. "Expected min_value to be lower than max_value, "
  54. "got %.4f and %.4f" % (min_value, max_value))
  55. eps = np.finfo(arr.dtype).eps
  56. components = arr.flat[0:50]
  57. beyond_min = np.min(components) < min_value - eps
  58. beyond_max = np.max(components) > max_value + eps
  59. if beyond_min or beyond_max:
  60. ia.warn(
  61. "Value range of heatmap was chosen to be (%.8f, %.8f), but "
  62. "found actual min/max of (%.8f, %.8f). Array will be "
  63. "clipped to chosen value range." % (
  64. min_value, max_value, np.min(arr), np.max(arr)))
  65. arr = np.clip(arr, min_value, max_value)
  66. if arr.ndim == 2:
  67. arr = arr[..., np.newaxis]
  68. self.arr_was_2d = True
  69. else:
  70. self.arr_was_2d = False
  71. min_is_zero = 0.0 - eps < min_value < 0.0 + eps
  72. max_is_one = 1.0 - eps < max_value < 1.0 + eps
  73. if min_is_zero and max_is_one:
  74. self.arr_0to1 = arr
  75. else:
  76. self.arr_0to1 = (arr - min_value) / (max_value - min_value)
  77. # don't allow arrays here as an alternative to tuples as input
  78. # as allowing arrays introduces risk to mix up 'arr' and 'shape' args
  79. self.shape = shape
  80. self.min_value = min_value
  81. self.max_value = max_value
  82. def get_arr(self):
  83. """Get the heatmap's array in value range provided to ``__init__()``.
  84. The :class:`HeatmapsOnImage` object saves heatmaps internally in the
  85. value range ``[0.0, 1.0]``. This function converts the internal
  86. representation to ``[min, max]``, where ``min`` and ``max`` are
  87. provided to :func:`HeatmapsOnImage.__init__` upon instantiation of
  88. the object.
  89. Returns
  90. -------
  91. (H,W) ndarray or (H,W,C) ndarray
  92. Heatmap array of dtype ``float32``.
  93. """
  94. if self.arr_was_2d and self.arr_0to1.shape[2] == 1:
  95. arr = self.arr_0to1[:, :, 0]
  96. else:
  97. arr = self.arr_0to1
  98. eps = np.finfo(np.float32).eps
  99. min_is_zero = 0.0 - eps < self.min_value < 0.0 + eps
  100. max_is_one = 1.0 - eps < self.max_value < 1.0 + eps
  101. if min_is_zero and max_is_one:
  102. return np.copy(arr)
  103. diff = self.max_value - self.min_value
  104. return self.min_value + diff * arr
  105. # TODO
  106. # def find_global_maxima(self):
  107. # raise NotImplementedError()
  108. def draw(self, size=None, cmap="jet"):
  109. """Render the heatmaps as RGB images.
  110. Parameters
  111. ----------
  112. size : None or float or iterable of int or iterable of float, optional
  113. Size of the rendered RGB image as ``(height, width)``.
  114. See :func:`~imgaug.imgaug.imresize_single_image` for details.
  115. If set to ``None``, no resizing is performed and the size of the
  116. heatmaps array is used.
  117. cmap : str or None, optional
  118. Name of the ``matplotlib`` color map to use when convert the
  119. heatmaps to RGB images.
  120. If set to ``None``, no color map will be used and the heatmaps
  121. will be converted to simple intensity maps.
  122. Returns
  123. -------
  124. list of (H,W,3) ndarray
  125. Rendered heatmaps as ``uint8`` arrays.
  126. Always a **list** containing one RGB image per heatmap array
  127. channel.
  128. """
  129. heatmaps_uint8 = self.to_uint8()
  130. heatmaps_drawn = []
  131. for c in sm.xrange(heatmaps_uint8.shape[2]):
  132. # We use c:c+1 here to get a (H,W,1) array. Otherwise imresize
  133. # would have to re-attach an axis.
  134. heatmap_c = heatmaps_uint8[..., c:c+1]
  135. if size is not None:
  136. heatmap_c_rs = ia.imresize_single_image(
  137. heatmap_c, size, interpolation="nearest")
  138. else:
  139. heatmap_c_rs = heatmap_c
  140. heatmap_c_rs = np.squeeze(heatmap_c_rs).astype(np.float32) / 255.0
  141. if cmap is not None:
  142. # import only when necessary (faster startup; optional
  143. # dependency; less fragile -- see issue #225)
  144. import matplotlib.pyplot as plt
  145. cmap_func = plt.get_cmap(cmap)
  146. heatmap_cmapped = cmap_func(heatmap_c_rs)
  147. heatmap_cmapped = np.delete(heatmap_cmapped, 3, 2)
  148. else:
  149. heatmap_cmapped = np.tile(
  150. heatmap_c_rs[..., np.newaxis], (1, 1, 3))
  151. heatmap_cmapped = np.clip(
  152. heatmap_cmapped * 255, 0, 255).astype(np.uint8)
  153. heatmaps_drawn.append(heatmap_cmapped)
  154. return heatmaps_drawn
  155. def draw_on_image(self, image, alpha=0.75, cmap="jet", resize="heatmaps"):
  156. """Draw the heatmaps as overlays over an image.
  157. Parameters
  158. ----------
  159. image : (H,W,3) ndarray
  160. Image onto which to draw the heatmaps.
  161. Expected to be of dtype ``uint8``.
  162. alpha : float, optional
  163. Alpha/opacity value to use for the mixing of image and heatmaps.
  164. Larger values mean that the heatmaps will be more visible and the
  165. image less visible.
  166. cmap : str or None, optional
  167. Name of the ``matplotlib`` color map to use.
  168. See :func:`HeatmapsOnImage.draw` for details.
  169. resize : {'heatmaps', 'image'}, optional
  170. In case of size differences between the image and heatmaps,
  171. either the image or the heatmaps can be resized. This parameter
  172. controls which of the two will be resized to the other's size.
  173. Returns
  174. -------
  175. list of (H,W,3) ndarray
  176. Rendered overlays as ``uint8`` arrays.
  177. Always a **list** containing one RGB image per heatmap array
  178. channel.
  179. """
  180. # assert RGB image
  181. assert image.ndim == 3, (
  182. "Expected to draw on three-dimensional image, "
  183. "got %d dimensions with shape %s instead." % (
  184. image.ndim, image.shape))
  185. assert image.shape[2] == 3, (
  186. "Expected RGB image, got %d channels instead." % (image.shape[2],))
  187. assert image.dtype.name == "uint8", (
  188. "Expected uint8 image, got dtype %s." % (image.dtype.name,))
  189. assert 0 - 1e-8 <= alpha <= 1.0 + 1e-8, (
  190. "Expected 'alpha' to be in the interval [0.0, 1.0], got %.4f" % (
  191. alpha))
  192. assert resize in ["heatmaps", "image"], (
  193. "Expected resize to be \"heatmaps\" or \"image\", "
  194. "got %s instead." % (resize,))
  195. if resize == "image":
  196. image = ia.imresize_single_image(
  197. image, self.arr_0to1.shape[0:2], interpolation="cubic")
  198. heatmaps_drawn = self.draw(
  199. size=image.shape[0:2] if resize == "heatmaps" else None,
  200. cmap=cmap)
  201. # TODO use blend_alpha here
  202. mix = [
  203. np.clip(
  204. (1-alpha) * image + alpha * heatmap_i,
  205. 0, 255
  206. ).astype(np.uint8)
  207. for heatmap_i
  208. in heatmaps_drawn]
  209. return mix
  210. def invert(self):
  211. """Invert each component in the heatmap.
  212. This shifts low values towards high values and vice versa.
  213. This changes each value to::
  214. v' = max - (v - min)
  215. where ``v`` is the value at a spatial location, ``min`` is the
  216. minimum value in the heatmap and ``max`` is the maximum value.
  217. As the heatmap uses internally a ``0.0`` to ``1.0`` representation,
  218. this simply becomes ``v' = 1.0 - v``.
  219. This function can be useful e.g. when working with depth maps, where
  220. algorithms might have an easier time representing the furthest away
  221. points with zeros, requiring an inverted depth map.
  222. Returns
  223. -------
  224. imgaug.augmentables.heatmaps.HeatmapsOnImage
  225. Inverted heatmap.
  226. """
  227. arr_inv = HeatmapsOnImage.from_0to1(
  228. 1 - self.arr_0to1,
  229. shape=self.shape,
  230. min_value=self.min_value,
  231. max_value=self.max_value)
  232. arr_inv.arr_was_2d = self.arr_was_2d
  233. return arr_inv
  234. def pad(self, top=0, right=0, bottom=0, left=0, mode="constant", cval=0.0):
  235. """Pad the heatmaps at their top/right/bottom/left side.
  236. Parameters
  237. ----------
  238. top : int, optional
  239. Amount of pixels to add at the top side of the heatmaps.
  240. Must be ``0`` or greater.
  241. right : int, optional
  242. Amount of pixels to add at the right side of the heatmaps.
  243. Must be ``0`` or greater.
  244. bottom : int, optional
  245. Amount of pixels to add at the bottom side of the heatmaps.
  246. Must be ``0`` or greater.
  247. left : int, optional
  248. Amount of pixels to add at the left side of the heatmaps.
  249. Must be ``0`` or greater.
  250. mode : string, optional
  251. Padding mode to use. See :func:`~imgaug.imgaug.pad` for details.
  252. cval : number, optional
  253. Value to use for padding `mode` is ``constant``.
  254. See :func:`~imgaug.imgaug.pad` for details.
  255. Returns
  256. -------
  257. imgaug.augmentables.heatmaps.HeatmapsOnImage
  258. Padded heatmaps of height ``H'=H+top+bottom`` and
  259. width ``W'=W+left+right``.
  260. """
  261. from ..augmenters import size as iasize
  262. arr_0to1_padded = iasize.pad(
  263. self.arr_0to1,
  264. top=top,
  265. right=right,
  266. bottom=bottom,
  267. left=left,
  268. mode=mode,
  269. cval=cval)
  270. # TODO change to deepcopy()
  271. return HeatmapsOnImage.from_0to1(
  272. arr_0to1_padded,
  273. shape=self.shape,
  274. min_value=self.min_value,
  275. max_value=self.max_value)
  276. def pad_to_aspect_ratio(self, aspect_ratio, mode="constant", cval=0.0,
  277. return_pad_amounts=False):
  278. """Pad the heatmaps until they match a target aspect ratio.
  279. Depending on which dimension is smaller (height or width), only the
  280. corresponding sides (left/right or top/bottom) will be padded. In
  281. each case, both of the sides will be padded equally.
  282. Parameters
  283. ----------
  284. aspect_ratio : float
  285. Target aspect ratio, given as width/height. E.g. ``2.0`` denotes
  286. the image having twice as much width as height.
  287. mode : str, optional
  288. Padding mode to use.
  289. See :func:`~imgaug.imgaug.pad` for details.
  290. cval : number, optional
  291. Value to use for padding if `mode` is ``constant``.
  292. See :func:`~imgaug.imgaug.pad` for details.
  293. return_pad_amounts : bool, optional
  294. If ``False``, then only the padded instance will be returned.
  295. If ``True``, a tuple with two entries will be returned, where
  296. the first entry is the padded instance and the second entry are
  297. the amounts by which each array side was padded. These amounts are
  298. again a tuple of the form ``(top, right, bottom, left)``, with
  299. each value being an integer.
  300. Returns
  301. -------
  302. imgaug.augmentables.heatmaps.HeatmapsOnImage
  303. Padded heatmaps as :class:`HeatmapsOnImage` instance.
  304. tuple of int
  305. Amounts by which the instance's array was padded on each side,
  306. given as a tuple ``(top, right, bottom, left)``.
  307. This tuple is only returned if `return_pad_amounts` was set to
  308. ``True``.
  309. """
  310. from ..augmenters import size as iasize
  311. arr_0to1_padded, pad_amounts = iasize.pad_to_aspect_ratio(
  312. self.arr_0to1,
  313. aspect_ratio=aspect_ratio,
  314. mode=mode,
  315. cval=cval,
  316. return_pad_amounts=True)
  317. # TODO change to deepcopy()
  318. heatmaps = HeatmapsOnImage.from_0to1(
  319. arr_0to1_padded,
  320. shape=self.shape,
  321. min_value=self.min_value,
  322. max_value=self.max_value)
  323. if return_pad_amounts:
  324. return heatmaps, pad_amounts
  325. return heatmaps
  326. def avg_pool(self, block_size):
  327. """Average-pool the heatmap(s) array using a given block/kernel size.
  328. Parameters
  329. ----------
  330. block_size : int or tuple of int
  331. Size of each block of values to pool, aka kernel size.
  332. See :func:`~imgaug.imgaug.pool` for details.
  333. Returns
  334. -------
  335. imgaug.augmentables.heatmaps.HeatmapsOnImage
  336. Heatmaps after average pooling.
  337. """
  338. arr_0to1_reduced = ia.avg_pool(self.arr_0to1, block_size, pad_cval=0.0)
  339. return HeatmapsOnImage.from_0to1(
  340. arr_0to1_reduced,
  341. shape=self.shape,
  342. min_value=self.min_value,
  343. max_value=self.max_value)
  344. def max_pool(self, block_size):
  345. """Max-pool the heatmap(s) array using a given block/kernel size.
  346. Parameters
  347. ----------
  348. block_size : int or tuple of int
  349. Size of each block of values to pool, aka kernel size.
  350. See :func:`~imgaug.imgaug.pool` for details.
  351. Returns
  352. -------
  353. imgaug.augmentables.heatmaps.HeatmapsOnImage
  354. Heatmaps after max-pooling.
  355. """
  356. arr_0to1_reduced = ia.max_pool(self.arr_0to1, block_size)
  357. return HeatmapsOnImage.from_0to1(
  358. arr_0to1_reduced,
  359. shape=self.shape,
  360. min_value=self.min_value,
  361. max_value=self.max_value)
  362. @ia.deprecated(alt_func="HeatmapsOnImage.resize()",
  363. comment="resize() has the exactly same interface.")
  364. def scale(self, *args, **kwargs):
  365. """Resize the heatmap(s) array given a target size and interpolation."""
  366. return self.resize(*args, **kwargs)
  367. def resize(self, sizes, interpolation="cubic"):
  368. """Resize the heatmap(s) array given a target size and interpolation.
  369. Parameters
  370. ----------
  371. sizes : float or iterable of int or iterable of float
  372. New size of the array in ``(height, width)``.
  373. See :func:`~imgaug.imgaug.imresize_single_image` for details.
  374. interpolation : None or str or int, optional
  375. The interpolation to use during resize.
  376. See :func:`~imgaug.imgaug.imresize_single_image` for details.
  377. Returns
  378. -------
  379. imgaug.augmentables.heatmaps.HeatmapsOnImage
  380. Resized heatmaps object.
  381. """
  382. arr_0to1_resized = ia.imresize_single_image(
  383. self.arr_0to1, sizes, interpolation=interpolation)
  384. # cubic interpolation can lead to values outside of [0.0, 1.0],
  385. # see https://github.com/opencv/opencv/issues/7195
  386. # TODO area interpolation too?
  387. arr_0to1_resized = np.clip(arr_0to1_resized, 0.0, 1.0)
  388. return HeatmapsOnImage.from_0to1(
  389. arr_0to1_resized,
  390. shape=self.shape,
  391. min_value=self.min_value,
  392. max_value=self.max_value)
  393. def to_uint8(self):
  394. """Convert this heatmaps object to an ``uint8`` array.
  395. Returns
  396. -------
  397. (H,W,C) ndarray
  398. Heatmap as an ``uint8`` array, i.e. with the discrete value
  399. range ``[0, 255]``.
  400. """
  401. # TODO this always returns (H,W,C), even if input ndarray was
  402. # originally (H,W). Does it make sense here to also return
  403. # (H,W) if self.arr_was_2d?
  404. arr_0to255 = np.clip(np.round(self.arr_0to1 * 255), 0, 255)
  405. arr_uint8 = arr_0to255.astype(np.uint8)
  406. return arr_uint8
  407. @staticmethod
  408. def from_uint8(arr_uint8, shape, min_value=0.0, max_value=1.0):
  409. """Create a ``float``-based heatmaps object from an ``uint8`` array.
  410. Parameters
  411. ----------
  412. arr_uint8 : (H,W) ndarray or (H,W,C) ndarray
  413. Heatmap(s) array, where ``H`` is height, ``W`` is width
  414. and ``C`` is the number of heatmap channels.
  415. Expected dtype is ``uint8``.
  416. shape : tuple of int
  417. Shape of the image on which the heatmap(s) is/are placed.
  418. **Not** the shape of the heatmap(s) array, unless it is identical
  419. to the image shape (note the likely difference between the arrays
  420. in the number of channels).
  421. If there is not a corresponding image, use the shape of the
  422. heatmaps array.
  423. min_value : float, optional
  424. Minimum value of the float heatmaps that the input array
  425. represents. This will usually be 0.0. In most other cases it will
  426. be close to the interval ``[0.0, 1.0]``.
  427. Calling :func:`~imgaug.HeatmapsOnImage.get_arr`, will automatically
  428. convert the interval ``[0.0, 1.0]`` float array to this
  429. ``[min, max]`` interval.
  430. max_value : float, optional
  431. Minimum value of the float heatmaps that the input array
  432. represents. This will usually be 1.0.
  433. See parameter `min_value` for details.
  434. Returns
  435. -------
  436. imgaug.augmentables.heatmaps.HeatmapsOnImage
  437. Heatmaps object.
  438. """
  439. arr_0to1 = arr_uint8.astype(np.float32) / 255.0
  440. return HeatmapsOnImage.from_0to1(
  441. arr_0to1, shape,
  442. min_value=min_value,
  443. max_value=max_value)
  444. @staticmethod
  445. def from_0to1(arr_0to1, shape, min_value=0.0, max_value=1.0):
  446. """Create a heatmaps object from a ``[0.0, 1.0]`` float array.
  447. Parameters
  448. ----------
  449. arr_0to1 : (H,W) or (H,W,C) ndarray
  450. Heatmap(s) array, where ``H`` is the height, ``W`` is the width
  451. and ``C`` is the number of heatmap channels.
  452. Expected dtype is ``float32``.
  453. shape : tuple of ints
  454. Shape of the image on which the heatmap(s) is/are placed.
  455. **Not** the shape of the heatmap(s) array, unless it is identical
  456. to the image shape (note the likely difference between the arrays
  457. in the number of channels).
  458. If there is not a corresponding image, use the shape of the
  459. heatmaps array.
  460. min_value : float, optional
  461. Minimum value of the float heatmaps that the input array
  462. represents. This will usually be 0.0. In most other cases it will
  463. be close to the interval ``[0.0, 1.0]``.
  464. Calling :func:`~imgaug.HeatmapsOnImage.get_arr`, will automatically
  465. convert the interval ``[0.0, 1.0]`` float array to this
  466. ``[min, max]`` interval.
  467. max_value : float, optional
  468. Minimum value of the float heatmaps that the input array
  469. represents. This will usually be 1.0.
  470. See parameter `min_value` for details.
  471. Returns
  472. -------
  473. imgaug.augmentables.heatmaps.HeatmapsOnImage
  474. Heatmaps object.
  475. """
  476. heatmaps = HeatmapsOnImage(arr_0to1, shape,
  477. min_value=0.0, max_value=1.0)
  478. heatmaps.min_value = min_value
  479. heatmaps.max_value = max_value
  480. return heatmaps
  481. # TODO change name to change_value_range()?
  482. @classmethod
  483. def change_normalization(cls, arr, source, target):
  484. """Change the value range of a heatmap array.
  485. E.g. the value range may be changed from the interval ``[0.0, 1.0]``
  486. to ``[-1.0, 1.0]``.
  487. Parameters
  488. ----------
  489. arr : ndarray
  490. Heatmap array to modify.
  491. source : tuple of float
  492. Current value range of the input array, given as a
  493. tuple ``(min, max)``, where both are ``float`` values.
  494. target : tuple of float
  495. Desired output value range of the array, given as a
  496. tuple ``(min, max)``, where both are ``float`` values.
  497. Returns
  498. -------
  499. ndarray
  500. Input array, with value range projected to the desired target
  501. value range.
  502. """
  503. assert ia.is_np_array(arr), (
  504. "Expected 'arr' to be an ndarray, got type %s." % (type(arr),))
  505. def _validate_tuple(arg_name, arg_value):
  506. assert isinstance(arg_value, tuple), (
  507. "'%s' was not a HeatmapsOnImage instance, "
  508. "expected type tuple then. Got type %s." % (
  509. arg_name, type(arg_value),))
  510. assert len(arg_value) == 2, (
  511. "Expected tuple '%s' to contain exactly two entries, "
  512. "got %d." % (arg_name, len(arg_value),))
  513. assert arg_value[0] < arg_value[1], (
  514. "Expected tuple '%s' to have two entries with "
  515. "entry 1 < entry 2, got values %.4f and %.4f." % (
  516. arg_name, arg_value[0], arg_value[1]))
  517. if isinstance(source, HeatmapsOnImage):
  518. source = (source.min_value, source.max_value)
  519. else:
  520. _validate_tuple("source", source)
  521. if isinstance(target, HeatmapsOnImage):
  522. target = (target.min_value, target.max_value)
  523. else:
  524. _validate_tuple("target", target)
  525. # Check if source and target are the same (with a tiny bit of
  526. # tolerance) if so, evade compuation and just copy the array instead.
  527. # This is reasonable, as source and target will often both
  528. # be (0.0, 1.0).
  529. eps = np.finfo(arr.dtype).eps
  530. mins_same = source[0] - 10*eps < target[0] < source[0] + 10*eps
  531. maxs_same = source[1] - 10*eps < target[1] < source[1] + 10*eps
  532. if mins_same and maxs_same:
  533. return np.copy(arr)
  534. min_source, max_source = source
  535. min_target, max_target = target
  536. diff_source = max_source - min_source
  537. diff_target = max_target - min_target
  538. arr_0to1 = (arr - min_source) / diff_source
  539. arr_target = min_target + arr_0to1 * diff_target
  540. return arr_target
  541. # TODO make this a proper shallow-copy
  542. def copy(self):
  543. """Create a shallow copy of the heatmaps object.
  544. Returns
  545. -------
  546. imgaug.augmentables.heatmaps.HeatmapsOnImage
  547. Shallow copy.
  548. """
  549. return self.deepcopy()
  550. def deepcopy(self):
  551. """Create a deep copy of the heatmaps object.
  552. Returns
  553. -------
  554. imgaug.augmentables.heatmaps.HeatmapsOnImage
  555. Deep copy.
  556. """
  557. return HeatmapsOnImage(
  558. self.get_arr(),
  559. shape=self.shape,
  560. min_value=self.min_value,
  561. max_value=self.max_value)