_map_array.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import numpy as np
  2. def map_array(input_arr, input_vals, output_vals, out=None):
  3. """Map values from input array from input_vals to output_vals.
  4. Parameters
  5. ----------
  6. input_arr : array of int, shape (M[, ...])
  7. The input label image.
  8. input_vals : array of int, shape (K,)
  9. The values to map from.
  10. output_vals : array, shape (K,)
  11. The values to map to.
  12. out : array, same shape as `input_arr`
  13. The output array. Will be created if not provided. It should
  14. have the same dtype as `output_vals`.
  15. Returns
  16. -------
  17. out : array, same shape as `input_arr`
  18. The array of mapped values.
  19. Notes
  20. -----
  21. If `input_arr` contains values that aren't covered by `input_vals`, they
  22. are set to 0.
  23. Examples
  24. --------
  25. >>> import numpy as np
  26. >>> import skimage as ski
  27. >>> ski.util.map_array(
  28. ... input_arr=np.array([[0, 2, 2, 0], [3, 4, 5, 0]]),
  29. ... input_vals=np.array([1, 2, 3, 4, 6]),
  30. ... output_vals=np.array([6, 7, 8, 9, 10]),
  31. ... )
  32. array([[0, 7, 7, 0],
  33. [8, 9, 0, 0]])
  34. """
  35. from ._remap import _map_array
  36. if not np.issubdtype(input_arr.dtype, np.integer):
  37. raise TypeError('The dtype of an array to be remapped should be integer.')
  38. # We ravel the input array for simplicity of iteration in Cython:
  39. orig_shape = input_arr.shape
  40. # NumPy docs for `np.ravel()` says:
  41. # "When a view is desired in as many cases as possible,
  42. # arr.reshape(-1) may be preferable."
  43. input_arr = input_arr.reshape(-1)
  44. if out is None:
  45. out = np.empty(orig_shape, dtype=output_vals.dtype)
  46. elif out.shape != orig_shape:
  47. raise ValueError(
  48. 'If out array is provided, it should have the same shape as '
  49. f'the input array. Input array has shape {orig_shape}, provided '
  50. f'output array has shape {out.shape}.'
  51. )
  52. try:
  53. out_view = out.view()
  54. out_view.shape = (-1,) # no-copy reshape/ravel
  55. except AttributeError: # if out strides are not compatible with 0-copy
  56. raise ValueError(
  57. 'If out array is provided, it should be either contiguous '
  58. f'or 1-dimensional. Got array with shape {out.shape} and '
  59. f'strides {out.strides}.'
  60. )
  61. # ensure all arrays have matching types before sending to Cython
  62. input_vals = input_vals.astype(input_arr.dtype, copy=False)
  63. output_vals = output_vals.astype(out.dtype, copy=False)
  64. _map_array(input_arr, out_view, input_vals, output_vals)
  65. return out
  66. class ArrayMap:
  67. """Class designed to mimic mapping by NumPy array indexing.
  68. This class is designed to replicate the use of NumPy arrays for mapping
  69. values with indexing:
  70. >>> values = np.array([0.25, 0.5, 1.0])
  71. >>> indices = np.array([[0, 0, 1], [2, 2, 1]])
  72. >>> values[indices]
  73. array([[0.25, 0.25, 0.5 ],
  74. [1. , 1. , 0.5 ]])
  75. The issue with this indexing is that you need a very large ``values``
  76. array if the values in the ``indices`` array are large.
  77. >>> values = np.array([0.25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.0])
  78. >>> indices = np.array([[0, 0, 10], [0, 10, 10]])
  79. >>> values[indices]
  80. array([[0.25, 0.25, 1. ],
  81. [0.25, 1. , 1. ]])
  82. Using this class, the approach is similar, but there is no need to
  83. create a large values array:
  84. >>> in_indices = np.array([0, 10])
  85. >>> out_values = np.array([0.25, 1.0])
  86. >>> values = ArrayMap(in_indices, out_values)
  87. >>> values
  88. ArrayMap(array([ 0, 10]), array([0.25, 1. ]))
  89. >>> print(values)
  90. ArrayMap:
  91. 0 → 0.25
  92. 10 → 1.0
  93. >>> indices = np.array([[0, 0, 10], [0, 10, 10]])
  94. >>> values[indices]
  95. array([[0.25, 0.25, 1. ],
  96. [0.25, 1. , 1. ]])
  97. Parameters
  98. ----------
  99. in_values : array of int, shape (K,)
  100. The source values from which to map.
  101. out_values : array, shape (K,)
  102. The destination values from which to map.
  103. """
  104. def __init__(self, in_values, out_values):
  105. self.in_values = in_values
  106. self.out_values = out_values
  107. self._max_str_lines = 4
  108. self._array = None
  109. def __len__(self):
  110. """Return one more than the maximum label value being remapped."""
  111. return np.max(self.in_values) + 1
  112. def __array__(self, dtype=None, copy=None):
  113. """Return an array that behaves like the arraymap when indexed.
  114. This array can be very large: it is the size of the largest value
  115. in the ``in_vals`` array, plus one.
  116. """
  117. if dtype is None:
  118. dtype = self.out_values.dtype
  119. output = np.zeros(np.max(self.in_values) + 1, dtype=dtype)
  120. output[self.in_values] = self.out_values
  121. return output
  122. @property
  123. def dtype(self):
  124. return self.out_values.dtype
  125. def __repr__(self):
  126. return f'ArrayMap({repr(self.in_values)}, {repr(self.out_values)})'
  127. def __str__(self):
  128. if len(self.in_values) <= self._max_str_lines + 1:
  129. rows = range(len(self.in_values))
  130. string = '\n'.join(
  131. ['ArrayMap:']
  132. + [f' {self.in_values[i]} → {self.out_values[i]}' for i in rows]
  133. )
  134. else:
  135. rows0 = list(range(0, self._max_str_lines // 2))
  136. rows1 = list(range(-self._max_str_lines // 2, 0))
  137. string = '\n'.join(
  138. ['ArrayMap:']
  139. + [f' {self.in_values[i]} → {self.out_values[i]}' for i in rows0]
  140. + [' ...']
  141. + [f' {self.in_values[i]} → {self.out_values[i]}' for i in rows1]
  142. )
  143. return string
  144. def __call__(self, arr):
  145. return self.__getitem__(arr)
  146. def __getitem__(self, index):
  147. scalar = np.isscalar(index)
  148. if scalar:
  149. index = np.array([index])
  150. elif isinstance(index, slice):
  151. start = index.start or 0 # treat None or 0 the same way
  152. stop = index.stop if index.stop is not None else len(self)
  153. step = index.step
  154. index = np.arange(start, stop, step)
  155. if index.dtype == bool:
  156. index = np.flatnonzero(index)
  157. out = map_array(
  158. index,
  159. self.in_values.astype(index.dtype, copy=False),
  160. self.out_values,
  161. )
  162. if scalar:
  163. out = out[0]
  164. return out
  165. def __setitem__(self, indices, values):
  166. if self._array is None:
  167. self._array = self.__array__()
  168. self._array[indices] = values
  169. self.in_values = np.flatnonzero(self._array)
  170. self.out_values = self._array[self.in_values]