Zero padding slice past end of array in numpy

Is there a way? Yes. Is it complicated? Not especially.

import numpy as np
def fill_crop(img, pos, crop):
  '''
  Fills `crop` with values from `img` at `pos`, 
  while accounting for the crop being off the edge of `img`.
  *Note:* negative values in `pos` are interpreted as-is, not as "from the end".
  '''
  img_shape, pos, crop_shape = np.array(img.shape), np.array(pos), np.array(crop.shape),
  end = pos+crop_shape
  # Calculate crop slice positions
  crop_low = np.clip(0 - pos, a_min=0, a_max=crop_shape)
  crop_high = crop_shape - np.clip(end-img_shape, a_min=0, a_max=crop_shape)
  crop_slices = (slice(low, high) for low, high in zip(crop_low, crop_high))
  # Calculate img slice positions
  pos = np.clip(pos, a_min=0, a_max=img_shape)
  end = np.clip(end, a_min=0, a_max=img_shape)
  img_slices = (slice(low, high) for low, high in zip(pos, end))
  crop[tuple(crop_slices)] = img[tuple(img_slices)]

Why use this?

If memory is a concern, then copying the image into a padded version might not be good. This also works well for higher dimensional inputs, and it's clear how to return indices/slices if you needed those.

Why is crop a parameter?

To indicate the padded value, we can instead create the memory for the crop ahead of time with np.zeros/np.full, then fill in the part that we need. The difficulty then isn't working out where to copy from, but instead, where to paste inside the crop.

Theory

Let's look at a 1D case:

Colourful description of 1D case

If you think about it a little bit, you can see that:

  • crop_low is as far above 0 as pos is below 0, but if pos >= 0, then crop_low == 0
  • crop_high is as far below crop.shape as end is above img.shape, but if end <= img.shape, then crop_high == crop.shape

If we put this into normal python code, it would look like this:

crop_low = max(-pos, 0)
crop_high = crop.shape - max(end-img.shape, 0)

The rest of the code above is just for indexing.

Testing

# Examples in 1 dimension
img = np.arange(10, 20)

# Normal
pos = np.array([1,])
crop = np.full([5,], 0)
fill_crop(img, pos, crop)
assert crop.tolist() == [11, 12, 13, 14, 15]

# Off end
pos = np.array([8,])
crop = np.full([5,], 0)
fill_crop(img, pos, crop)
assert crop.tolist() == [18, 19,  0,  0,  0]

# Off start
pos = np.array([-2,])
crop = np.full([5,], 0)
fill_crop(img, pos, crop)
assert crop.tolist() == [ 0,  0, 10, 11, 12]


# Example in 2 dimensions (y,x)
img = np.arange(10, 10+10*10)\
          .reshape([10, 10])
# Off Top right
pos = np.array([-2, 8])
crop = np.full([5, 5], 0)
fill_crop(img, pos, crop)
assert np.all(crop[:2] == 0) # That is, the top two rows are 0s
assert np.all(crop[:, 3:] == 0) # That is, the right 3 rows are 0s
assert np.all(crop[2:, :2] == img[:3, 8:]) 
# That is, the rows 2-5 and columns 0-1 in the crop
#  are the same as the top 3 rows and columns 8 and 9 (the last two columns)

And there we have it. The over-engineered answer to the original question.


This class can handle your first test (x[1:4, 1:4]) and can be modified to handle your other test (i.e. appending zeros to the start) if you so desire.

class CustomArray():

    def __init__(self, numpy_array):
        self._array = numpy_array

    def __getitem__(self, val):

        # Get the shape you wish to return
        required_shape = []
        for i in range(2):
            start = val[i].start
            if not start:
                start = 0
            required_shape.append(val[i].stop - start)

        get = self._array[val]

        # Check first dimension
        while get.shape[0] < required_shape[0]:
            get = np.concatenate((get, np.zeros((1, get.shape[1]))))

        # Check second dimension
        get = get.T
        while get.shape[0] < required_shape[1]:
            get = np.concatenate((get, np.zeros((1, get.shape[1]))))
        get = get.T

        return get

Here is an example of it's usage:

a = CustomArray(np.ones((3, 3)))

print(a[:2, :2])
[[ 1.  1.]
 [ 1.  1.]]

print(a[:4, 1:6])
[[ 1.  1.  0.  0.  0.]
 [ 1.  1.  0.  0.  0.]
 [ 1.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.]]

# The actual numpy array is stored in the _array attribute
actual_numpy_array = a._array

As far as I know there is no numpy solution (nor in any package I know) for such a problem. You could do it yourself but it will be a really, really complicated one even if you only want basic slicing. I would suggest you manually np.pad your array and simply offset your start/stop/step before you actually slice it.

However if all you need to support are integers and slices without step I have some "working code" for this:

import numpy as np

class FunArray(np.ndarray):
    def __getitem__(self, item):

        all_in_slices = []
        pad = []
        for dim in range(self.ndim):
            # If the slice has no length then it's a single argument.
            # If it's just an integer then we just return, this is
            # needed for the representation to work properly
            # If it's not then create a list containing None-slices
            # for dim>=1 and continue down the loop
            try:
                len(item)
            except TypeError:
                if isinstance(item, int):
                    return super().__getitem__(item)
                newitem = [slice(None)]*self.ndim
                newitem[0] = item
                item = newitem
            # We're out of items, just append noop slices
            if dim >= len(item):
                all_in_slices.append(slice(0, self.shape[dim]))
                pad.append((0, 0))
            # We're dealing with an integer (no padding even if it's
            # out of bounds)
            if isinstance(item[dim], int):
                all_in_slices.append(slice(item[dim], item[dim]+1))
                pad.append((0, 0))
            # Dealing with a slice, here it get's complicated, we need
            # to correctly deal with None start/stop as well as with
            # out-of-bound values and correct padding
            elif isinstance(item[dim], slice):
                # Placeholders for values
                start, stop = 0, self.shape[dim]
                this_pad = [0, 0]
                if item[dim].start is None:
                    start = 0
                else:
                    if item[dim].start < 0:
                        this_pad[0] = -item[dim].start
                        start = 0
                    else:
                        start = item[dim].start
                if item[dim].stop is None:
                    stop = self.shape[dim]
                else:
                    if item[dim].stop > self.shape[dim]:
                        this_pad[1] = item[dim].stop - self.shape[dim]
                        stop = self.shape[dim]
                    else:
                        stop = item[dim].stop
                all_in_slices.append(slice(start, stop))
                pad.append(tuple(this_pad))

        # Let numpy deal with slicing
        ret = super().__getitem__(tuple(all_in_slices))
        # and padding
        ret = np.pad(ret, tuple(pad), mode='constant', constant_values=0)

        return ret

This can be used as follows:

>>> x = np.arange(9).reshape(3, 3)
>>> x = x.view(FunArray)
>>> x[0:2]
array([[0, 1, 2],
       [3, 4, 5]])
>>> x[-3:2]
array([[0, 0, 0],
       [0, 0, 0],
       [0, 0, 0],
       [0, 1, 2],
       [3, 4, 5]])
>>> x[-3:2, 2]
array([[0],
       [0],
       [0],
       [2],
       [5]])
>>> x[-1:4, -1:4]
array([[0, 0, 0, 0, 0],
       [0, 0, 1, 2, 0],
       [0, 3, 4, 5, 0],
       [0, 6, 7, 8, 0],
       [0, 0, 0, 0, 0]])

Note that this may be contain Bugs and "not cleanly coded" parts, I've never used this except in trivial cases.

Tags:

Python

Numpy