187 lines
6.2 KiB
Python
187 lines
6.2 KiB
Python
import numpy as np
|
|
from ._remap import _map_array
|
|
|
|
|
|
def map_array(input_arr, input_vals, output_vals, out=None):
|
|
"""Map values from input array from input_vals to output_vals.
|
|
|
|
Parameters
|
|
----------
|
|
input_arr : array of int, shape (M[, N][, P][, ...])
|
|
The input label image.
|
|
input_vals : array of int, shape (N,)
|
|
The values to map from.
|
|
output_vals : array, shape (N,)
|
|
The values to map to.
|
|
out: array, same shape as `input_arr`
|
|
The output array. Will be created if not provided. It should
|
|
have the same dtype as `output_vals`.
|
|
|
|
Returns
|
|
-------
|
|
out : array, same shape as `input_arr`
|
|
The array of mapped values.
|
|
"""
|
|
|
|
if not np.issubdtype(input_arr.dtype, np.integer):
|
|
raise TypeError(
|
|
'The dtype of an array to be remapped should be integer.'
|
|
)
|
|
# We ravel the input array for simplicity of iteration in Cython:
|
|
orig_shape = input_arr.shape
|
|
# NumPy docs for `np.ravel()` says:
|
|
# "When a view is desired in as many cases as possible,
|
|
# arr.reshape(-1) may be preferable."
|
|
input_arr = input_arr.reshape(-1)
|
|
if out is None:
|
|
out = np.empty(orig_shape, dtype=output_vals.dtype)
|
|
elif out.shape != orig_shape:
|
|
raise ValueError(
|
|
'If out array is provided, it should have the same shape as '
|
|
f'the input array. Input array has shape {orig_shape}, provided '
|
|
f'output array has shape {out.shape}.'
|
|
)
|
|
try:
|
|
out_view = out.view()
|
|
out_view.shape = (-1,) # no-copy reshape/ravel
|
|
except AttributeError: # if out strides are not compatible with 0-copy
|
|
raise ValueError(
|
|
'If out array is provided, it should be either contiguous '
|
|
f'or 1-dimensional. Got array with shape {out.shape} and '
|
|
f'strides {out.strides}.'
|
|
)
|
|
|
|
# ensure all arrays have matching types before sending to Cython
|
|
input_vals = input_vals.astype(input_arr.dtype, copy=False)
|
|
output_vals = output_vals.astype(out.dtype, copy=False)
|
|
_map_array(input_arr, out_view, input_vals, output_vals)
|
|
return out
|
|
|
|
|
|
class ArrayMap:
|
|
"""Class designed to mimic mapping by NumPy array indexing.
|
|
|
|
This class is designed to replicate the use of NumPy arrays for mapping
|
|
values with indexing:
|
|
|
|
>>> values = np.array([0.25, 0.5, 1.0])
|
|
>>> indices = np.array([[0, 0, 1], [2, 2, 1]])
|
|
>>> values[indices]
|
|
array([[0.25, 0.25, 0.5 ],
|
|
[1. , 1. , 0.5 ]])
|
|
|
|
The issue with this indexing is that you need a very large ``values``
|
|
array if the values in the ``indices`` array are large.
|
|
|
|
>>> values = np.array([0.25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.0])
|
|
>>> indices = np.array([[0, 0, 10], [0, 10, 10]])
|
|
>>> values[indices]
|
|
array([[0.25, 0.25, 1. ],
|
|
[0.25, 1. , 1. ]])
|
|
|
|
Using this class, the approach is similar, but there is no need to
|
|
create a large values array:
|
|
|
|
>>> in_indices = np.array([0, 10])
|
|
>>> out_values = np.array([0.25, 1.0])
|
|
>>> values = ArrayMap(in_indices, out_values)
|
|
>>> values
|
|
ArrayMap(array([ 0, 10]), array([0.25, 1. ]))
|
|
>>> print(values)
|
|
ArrayMap:
|
|
0 → 0.25
|
|
10 → 1.0
|
|
>>> indices = np.array([[0, 0, 10], [0, 10, 10]])
|
|
>>> values[indices]
|
|
array([[0.25, 0.25, 1. ],
|
|
[0.25, 1. , 1. ]])
|
|
|
|
Parameters
|
|
----------
|
|
in_values : array of int, shape (N,)
|
|
The source values from which to map.
|
|
out_values : array, shape (N,)
|
|
The destination values from which to map.
|
|
"""
|
|
def __init__(self, in_values, out_values):
|
|
self.in_values = in_values
|
|
self.out_values = out_values
|
|
self._max_str_lines = 4
|
|
self._array = None
|
|
|
|
def __len__(self):
|
|
"""Return one more than the maximum label value being remapped."""
|
|
return np.max(self.in_values) + 1
|
|
|
|
def __array__(self, dtype=None):
|
|
"""Return an array that behaves like the arraymap when indexed.
|
|
|
|
This array can be very large: it is the size of the largest value
|
|
in the ``in_vals`` array, plus one.
|
|
"""
|
|
if dtype is None:
|
|
dtype = self.out_values.dtype
|
|
output = np.zeros(np.max(self.in_values) + 1, dtype=dtype)
|
|
output[self.in_values] = self.out_values
|
|
return output
|
|
|
|
@property
|
|
def dtype(self):
|
|
return self.out_values.dtype
|
|
|
|
def __repr__(self):
|
|
return f'ArrayMap({repr(self.in_values)}, {repr(self.out_values)})'
|
|
|
|
def __str__(self):
|
|
if len(self.in_values) <= self._max_str_lines + 1:
|
|
rows = range(len(self.in_values))
|
|
string = '\n'.join(
|
|
['ArrayMap:'] +
|
|
[f' {self.in_values[i]} → {self.out_values[i]}' for i in rows]
|
|
)
|
|
else:
|
|
rows0 = list(range(0, self._max_str_lines // 2))
|
|
rows1 = list(range(-self._max_str_lines // 2, 0))
|
|
string = '\n'.join(
|
|
['ArrayMap:'] +
|
|
[f' {self.in_values[i]} → {self.out_values[i]}'
|
|
for i in rows0] +
|
|
[' ...'] +
|
|
[f' {self.in_values[i]} → {self.out_values[i]}'
|
|
for i in rows1]
|
|
)
|
|
return string
|
|
|
|
def __call__(self, arr):
|
|
return self.__getitem__(arr)
|
|
|
|
def __getitem__(self, index):
|
|
scalar = np.isscalar(index)
|
|
if scalar:
|
|
index = np.array([index])
|
|
elif isinstance(index, slice):
|
|
start = index.start or 0 # treat None or 0 the same way
|
|
stop = (index.stop
|
|
if index.stop is not None
|
|
else len(self))
|
|
step = index.step
|
|
index = np.arange(start, stop, step)
|
|
if index.dtype == bool:
|
|
index = np.flatnonzero(index)
|
|
|
|
out = map_array(
|
|
index,
|
|
self.in_values.astype(index.dtype, copy=False),
|
|
self.out_values,
|
|
)
|
|
|
|
if scalar:
|
|
out = out[0]
|
|
return out
|
|
|
|
def __setitem__(self, indices, values):
|
|
if self._array is None:
|
|
self._array = self.__array__()
|
|
self._array[indices] = values
|
|
self.in_values = np.flatnonzero(self._array)
|
|
self.out_values = self._array[self.in_values]
|