【问题标题】:Zero padding slice past end of array in numpynumpy中数组末尾的零填充切片
【发布时间】:2016-12-14 23:07:44
【问题描述】:

在 numpy 中,如果我在数组末尾切片,有没有办法将填充条目归零,这样我得到的东西就是所需切片的大小?

例如,

>>> x = np.ones((3,3,))
>>> x
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.],
       [ 1.,  1.,  1.]])
>>> x[1:4, 1:4] # would behave as x[1:3, 1:3] by default
array([[ 1.,  1.,  0.],
       [ 1.,  1.,  0.],
       [ 0.,  0.,  0.]])
>>> x[-1:2, -1:2]
 array([[ 0.,  0.,  0.],
       [ 0.,  1.,  1.],
       [ 0.,  1.,  1.]])

在视觉上,我希望越界区域为零填充:

我正在处理图像,并希望零填充以表示为我的应用程序移出图像。

我目前的计划是在切片之前使用 np.pad 使整个数组变大,但索引似乎有点棘手。有没有更简单的方法?

【问题讨论】:

  • 恐怕手动填充是你唯一的选择,因为在 NumPy 数组上修改 [] 的行为是不可能的(因为 numpy.ndarray 是在 C 中实现的,它禁止动态替换 numpy.ndarray.__getitem__) .

标签: python numpy


【解决方案1】:

据我所知,没有针对此类问题的 numpy 解决方案(也没有在我知道的任何包中)。你可以自己做,但即使你只想要基本的切片,它也会非常非常复杂。我建议您手动 np.pad 您的数组,并在实际切片之前简单地偏移您的开始/停止/步骤。

但是,如果您需要支持的只是整数和切片而无需步骤,那么我有一些“工作代码”:

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

可以这样使用:

>>> 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]])

请注意,这可能包含错误和“未完全编码”的部分,除了在琐碎的情况下,我从未使用过它。

【讨论】:

  • 代码看起来很复杂。这将使调试变得困难,并且可能会出现边缘情况错误。通过快速修补它,我发现它不适用于 3D 图像。 &gt;&gt;&gt; x = np.arange(9*3).reshape(3, 3, 3) &gt;&gt;&gt; x = x.view(FunArray) &gt;&gt;&gt; x[0:3, 0:3]试试这个sn-p,它会死。
  • @off99555 是的,它只是涵盖了“琐碎”的案例。它需要与维度一样多的切片,例如:x[0:3, 0:3, :]f[0:3, 0:3, 0:3]。模仿纯 NumPy 切片并非易事,在任何情况下对其进行扩展都会使其非常复杂——这就是代码已经相当复杂的原因。
【解决方案2】:

此类可以处理您的第一个测试 (x[1:4, 1:4]),如果您愿意,可以修改以处理您的其他测试(即在开头附加零)。

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

以下是它的用法示例:

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

【讨论】:

    【解决方案3】:

    有办法吗?是的。复杂吗?不是特别。

    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)]
    

    为什么要使用这个?

    如果内存是一个问题,那么将图像复制到填充版本可能并不好。这也适用于更高维度的输入,并且很清楚如何在需要时返回索引/切片。

    为什么crop是个参数?

    为了表示填充值,我们可以使用np.zeros/np.full提前为裁剪创建内存,然后填写我们需要的部分。困难不在于从哪里复制,而是从哪里粘贴到作物内部。

    理论

    让我们看一个一维案例:

    如果你稍微考虑一下,你会发现:

    • crop_low 远高于0,而pos 低于0,但如果pos &gt;= 0,则crop_low == 0
    • crop_high 远低于crop.shape 就像end 高于img.shape,但如果end &lt;= img.shape,则crop_high == crop.shape

    如果我们把它放到普通的python代码中,它会是这样的:

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

    上面的其余代码仅用于索引。

    测试

    # 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)
    

    我们有它。对原始问题的过度设计的答案。

    【讨论】:

    • 考虑到效率的好解决方案。虽然如果你把它打包成一个函数或一个与numpy数组有类似接口的类会更好。这对我来说非常有用,可以立即复制您的代码而无需了解任何内容。大声笑
    • @off99555 我不确定你的意思。我开始使用的功能是否在某些方面不足?
    • 我的意思是你应该让别人可以很容易地复制这段代码并使用它。例如,您可以在函数内部创建零数组并返回它,而不是要求用户提供它。
    • 我没有选择那个界面有两个原因。 1.您可能不希望图像区域之外的0。 2. 我鼓励这个而不是相对简单的填充选项作为内存有效的解决方案。此接口允许您将视图传递到连续的内存块中,因此如果您有多个裁剪,与创建几个小的 crops 然后稍后将它们连接起来相比,您可以节省创建一些内存。
    【解决方案4】:

    对于 2 或 3 级图像的最简单情况,以下是如何使用越界索引实现零填充“切片”的示例:

    def padded_slice(img, sl):
        output_shape = np.asarray(img.shape)
        output_shape[0] = sl[1] - sl[0]
        output_shape[1] = sl[3] - sl[2]
        src = [max(sl[0], 0),
               min(sl[1], img.shape[0]),
               max(sl[2], 0),
               min(sl[3], img.shape[1])]
        dst = [src[0] - sl[0], src[1] - sl[0],
               src[2] - sl[2], src[3] - sl[2]]
        output = np.zeros(output_shape, dtype=img.dtype)
        output[dst[0]:dst[1], dst[2]:dst[3]] = img[src[0]:src[1], src[2]:src[3]]
        return output
    

    例如,在 100x100 图像上使用 padded_slice(img, [-10, 150, -10, 150]) 调用此函数,它将返回 160x160 零填充图像。

    【讨论】:

    • ...在 100x100 图像上,它将返回 160x160 零填充图像。。很确定 OP 在这种情况下仍然想要一个 100x100 数组,而不是 160x160
    • @smac89 在原始示例中,他们说他们想要一个“所需切片的大小”的输出,而不是输入的大小。这种解释也与其他答案的解释相匹配。
    【解决方案5】:

    如果是一维数组,我这样做了,如果有人跌倒在这里会很有用...

    def getPaddedSlice(npArray, pos, lenSegment, center = False):
        lenNpArray = len(npArray)
        if center:
            if lenSegment % 2 == 0:
                startIndex = int(pos - math.floor(lenSegment / 2.0)) + 1 
                lastIndex  = int(pos + math.ceil(lenSegment / 2.0))  + 1  
    
            else : 
                startIndex = int(pos - math.floor(lenSegment / 2.0))
                lastIndex  = int(pos + math.ceil(lenSegment / 2.0)) + 1 
        else:
            startIndex = pos
            lastIndex  = startIndex + lenSegment 
    
        if startIndex < 0:
            padded_slice = npArray[0: lastIndex]
            padded_slice = np.concatenate((np.zeros(abs(startIndex)), padded_slice))  
        else:
            if center :
                padded_slice = npArray[startIndex: lastIndex]
            else:
                padded_slice = npArray[pos: lastIndex]
    
        if lastIndex > len(npArray):
            if center :
                padded_slice = npArray[startIndex: pos + lenSegment]
                padded_slice = np.concatenate((padded_slice, np.zeros(lastIndex - len(a))))
            else : 
                padded_slice = npArray[pos: pos + lenSegment]
                padded_slice = np.concatenate((padded_slice, np.zeros(lastIndex - len(a))))
    
        return padded_slice
    

    用法

    a = np.asarray([2,2,3,1,7,6,5,4])
    
    for i in range(len(a)):
        b = getPaddedSlice(a, i, lenSegment, True)
        print b
    

    显示

    [0 2 2 3]
    [2 2 3 1]
    [2 3 1 7]
    [3 1 7 6]
    [1 7 6 5]
    [7 6 5 4]
    [6 5 4 0]
    [5 4 0 0]
    

    【讨论】:

      猜你喜欢
      • 2011-08-06
      • 2017-06-17
      • 2016-11-06
      • 2018-10-05
      • 2017-10-24
      • 1970-01-01
      • 2014-12-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多