访问numpy数组的相邻单元格

时间:2018-10-25 05:28:01

标签: python numpy

如何有效地访问和修改2D numpy数组的周围8个单元格?

我有一个二维的numpy数组,如下所示:

arr = np.random.rand(720, 1440)

对于每个网格单元,我希望将周围的8个单元(中心单元减少)减少10%的中心单元,但前提是周围的单元格值超过0.25。我怀疑这样做的唯一方法是使用for循环,但想看看是否有更好/更快的解决方案。

-编辑:对于基于循环的soln:

arr = np.random.rand(720, 1440)

for (x, y), value in np.ndenumerate(arr):
    # Find 10% of current cell
    reduce_by = value * 0.1

    # Reduce the nearby 8 cells by 'reduce_by' but only if the cell value exceeds 0.25
    # [0] [1] [2]
    # [3] [*] [5]
    # [6] [7] [8]
    # * refers to current cell

    # cell [0]
    arr[x-1][y+1] = arr[x-1][y+1] * reduce_by if arr[x-1][y+1] > 0.25 else arr[x-1][y+1]

    # cell [1]
    arr[x][y+1] = arr[x][y+1] * reduce_by if arr[x][y+1] > 0.25 else arr[x][y+1]

    # cell [2]
    arr[x+1][y+1] = arr[x+1][y+1] * reduce_by if arr[x+1][y+1] > 0.25 else arr[x+1][y+1]

    # cell [3]
    arr[x-1][y] = arr[x-1][y] * reduce_by if arr[x-1][y] > 0.25 else arr[x-1][y]

    # cell [4] or current cell
    # do nothing

    # cell [5]
    arr[x+1][y] = arr[x+1][y] * reduce_by if arr[x+1][y] > 0.25 else arr[x+1][y]

    # cell [6]
    arr[x-1][y-1] = arr[x-1][y-1] * reduce_by if arr[x-1][y-1] > 0.25 else arr[x-1][y-1]

    # cell [7]
    arr[x][y-1] = arr[x][y-1] * reduce_by if arr[x][y-1] > 0.25 else arr[x][y-1]

    # cell [8]
    arr[x+1][y-1] = arr[x+1][y-1] * reduce_by if arr[x+1][y-1] > 0.25 else arr[x+1][y-1]

9 个答案:

答案 0 :(得分:4)

请澄清您的问题

  • 正如@jakevdp在评论中所提到的,是否真的打算让一个循环迭代依赖于另一个循环?
  • 如果是这种情况,应该如何精确处理边框像素?由于一个循环与另一个循环之间的依赖性,这将影响整个结果
  • 请添加一个有效的参考实现(参考实现中出现错误)

边界未触及,dependend循环迭代

除了以这种方式使用编译器外,我没有其他任何方式。在此示例中,我使用Numba,但如果已预先设置,您也可以在Cython中进行相同的操作。

import numpy as np
import numba as nb

@nb.njit(fastmath=True)
def without_borders(arr):
  for x in range(1,arr.shape[0]-1):
    for y in range(1,arr.shape[1]-1):
      # Find 10% of current cell
      reduce_by = arr[x,y] * 0.1

      # Reduce the nearby 8 cells by 'reduce_by' but only if the cell value exceeds 0.25
      # [0] [1] [2]
      # [3] [*] [5]
      # [6] [7] [8]
      # * refers to current cell

      # cell [0]
      arr[x-1][y+1] = arr[x-1][y+1] * reduce_by if arr[x-1][y+1] > 0.25 else arr[x-1][y+1]

      # cell [1]
      arr[x][y+1] = arr[x][y+1] * reduce_by if arr[x][y+1] > 0.25 else arr[x][y+1]

      # cell [2]
      arr[x+1][y+1] = arr[x+1][y+1] * reduce_by if arr[x+1][y+1] > 0.25 else arr[x+1][y+1]

      # cell [3]
      arr[x-1][y] = arr[x-1][y] * reduce_by if arr[x-1][y] > 0.25 else arr[x-1][y]

      # cell [4] or current cell
      # do nothing

      # cell [5]
      arr[x+1][y] = arr[x+1][y] * reduce_by if arr[x+1][y] > 0.25 else arr[x+1][y]

      # cell [6]
      arr[x-1][y-1] = arr[x-1][y-1] * reduce_by if arr[x-1][y-1] > 0.25 else arr[x-1][y-1]

      # cell [7]
      arr[x][y-1] = arr[x][y-1] * reduce_by if arr[x][y-1] > 0.25 else arr[x][y-1]

      # cell [8]
      arr[x+1][y-1] = arr[x+1][y-1] * reduce_by if arr[x+1][y-1] > 0.25 else arr[x+1][y-1]
  return arr

时间

arr = np.random.rand(720, 1440)

#non-compiled verson: 6.7s
#compiled version:    6ms (the first call takes about 450ms due to compilation overhead)

这真的很容易做到,给出的系数是1000倍。根据前三点,可能还会有更多的优化可能。

答案 1 :(得分:3)

不需要循环,避免通常的python循环,它们非常慢。为了获得更高的效率,请尽可能使用numpy的内置矩阵运算,“通用”函数,过滤器,掩码和条件。 https://realpython.com/numpy-array-programmin对于复杂的计算,矢量化还不错,请参见一些图表和基准测试Most efficient way to map function over numpy array(只是不要将其用于更简单的矩阵运算,如单元平方,内置函数将表现不佳)

容易看到,每个内部单元将由于8个邻居(因此减少0.1)而乘以0.9,最多8倍,并且另外由于是中央单元, 但是不能将其降低到.25 / .9 = 5/18以下。对于边界和角落单元格减少的数量分别下降到6和3倍。

因此

x1 = 700  # for debugging use lesser arrays
x2 = 1400

neighbors = 8 # each internal cell has 8 neighbors


for i in range(neighbors):
     view1 = arr[1:-1, 1:-1] # internal cells only
     arr [1:x1, 1:-1] = np.multiply(view1,.9, where = view1 > .25)

arr [1:-1, 1:-1] *= .9 

对边界和拐角的处理方式相同,分别是邻居= 5和3,并且视图不同。我猜这三种情况都可以在一个复杂的情况下合并为一个公式,但是速度会适中,因为边界和角仅占所有单元格的一小部分。

在这里,我使用了一个小循环,但它只重复了8次。应该使用功率,对数,整数部分和max函数也可以摆脱循环,从而导致笨拙,但单线速度更快,大约是

      numpy.multiply( view1, x ** numpy.max( numpy.ceil( (numpy.log (* view1/x... / log(.9)

我们还可以尝试另一种有用的技术,矢量化。 向量化正在构建一个函数,然后可以将其应用于数组的所有元素。

要进行更改,请让预设的边距/阈值找出要乘以的确切系数。这是什么样的代码

n = 8
decrease_by = numpy.logspace(1,N,num=n, base=x, endpoint=False)

margins = decrease_by * .25

# to do : save border rows for further analysis, skip this for simplicity now    
view1 = a [1: -1, 1: -1]

def decrease(x):


    k = numpy.searchsorted(margin, a)
    return x * decrease_by[k]

f = numpy.vectorize(decrease)
f(view1)

注释1 人们可以尝试使用不同的方法组合,例如将预计算的边距与矩阵算法结合使用,而不是与向量化结合使用。也许还有更多的技巧可以略微加快上述解决方案或上述解决方案的组合。

备注2 PyTorch与Numpy功能有很多相似之处,但可以从GPU中受益匪浅。如果您拥有不错的GPU,请考虑使用PyTorch。有人尝试过基于gpu的numpy(胶粘剂,废弃的gnumpy,minpy)。 https://stsievert.com/blog/2016/07/01/numpy-gpu/

答案 2 :(得分:1)

编辑:啊,我看到当您说“减少”时,您的意思是乘而不是减。我也没有意识到您想减少化合物的含量,而这种解决方案是不可行的。因此,这是不正确的,但是如果有帮助,我会保留。

您可以使用scipy.signal.convolve2d以矢量化的方式执行此操作:

import numpy as np
from scipy.signal import convolve2d

arr = np.random.rand(720, 1440)

mask = np.zeros((arr.shape[0] + 2, arr.shape[1] + 2))
mask[1:-1, 1:-1] = arr
mask[mask < 0.25] = 0
conv = np.ones((3, 3))
conv[1, 1] = 0

arr -= 0.1 * convolve2d(mask, conv, mode='valid')

这是从相反的角度思考问题的原因:每个平方应减去所有周围值的0.1倍。 conv数组对此进行编码,然后使用mask将其滑过scipy.signal.convolve2d数组以累加应减去的值。

答案 3 :(得分:1)

您的数组大小是典型的屏幕大小,因此我想单元格是[0,1)范围内的像素值。现在,像素值永远不会相乘。如果是,则操作将取决于范围(例如[0,1]或[0,255]),但它们永远不会。因此,我假设当您说“减少10%的单元格”时,您的意思是“减去10%的单元格”。但是即使如此,该操作仍然取决于它应用于单元格的顺序,因为先计算单元格总变化然后再应用它的常用方法(如卷积)会导致某些单元格值变为负数(例如0.251-8 * 0.1 * 0.999),如果它们是像素,则没有意义。

现在让我假设,您真的想使每个单元格彼此相乘并乘以一个因子,并且您想通过首先让每个单元格受其邻居数字0(您的单元格)影响来进行此操作编号),然后按其邻居编号1依次类推,对于邻居编号2、3、5、7和8依此类推。通常,从目标单元格的“角度”定义这种操作要比定义其简单从源细胞的。由于numpy在完整阵列(或其视图)上快速运行,因此,执行此操作的方法是将所有邻居移动到要修改的单元格位置。 Numpy没有shift(),但是它有一个roll(),就我们的目的而言,它也是一样的,因为我们不在乎边界单元,根据您的评论,边界单元可以恢复到最后的原始值。这是代码:

import numpy as np

arr = np.random.rand(720, 1440)
threshold = 0.25
factor    = 0.1
#                                                0 1 2
#                                    neighbors:  3   5
#                                                6 7 8
#                                                       ∆y  ∆x    axes
arr0 = np.where(arr  > threshold, arr  * np.roll(arr,   (1,  1), (0, 1)) * factor, arr)
arr1 = np.where(arr0 > threshold, arr0 * np.roll(arr0,   1,       0    ) * factor, arr0)
arr2 = np.where(arr1 > threshold, arr1 * np.roll(arr1,  (1, -1), (0, 1)) * factor, arr1)
arr3 = np.where(arr2 > threshold, arr2 * np.roll(arr2,       1,      1 ) * factor, arr2)
arr5 = np.where(arr3 > threshold, arr3 * np.roll(arr3,      -1,      1 ) * factor, arr3)
arr6 = np.where(arr5 > threshold, arr5 * np.roll(arr5, (-1,  1), (0, 1)) * factor, arr5)
arr7 = np.where(arr6 > threshold, arr6 * np.roll(arr6,  -1,       0    ) * factor, arr6)
res  = np.where(arr7 > threshold, arr7 * np.roll(arr7, (-1, -1), (0, 1)) * factor, arr7)
# fix the boundary:
res[:,  0] = arr[:,  0]
res[:, -1] = arr[:, -1]
res[ 0, :] = arr[ 0, :]
res[-1, :] = arr[-1, :]

请注意,即使如此,主要步骤仍与您在解决方案中所做的不同。但是它们确实是必须的,因为用numpy重写您的解决方案将导致在同一操作中读写数组,而numpy不能以可预测的方式进行操作。

如果您要改变主意,决定减而不是乘,则只需将*之前的np.roll列更改为-列。但这只是朝着适当的卷积方向(对2D图像进行的常见且重要的操作)方向的第一步,不过您将需要完全重新制定问题。

两个注意事项:在示例代码中,您像arr[x][y]那样对数组建立了索引,但是在numpy数组中,默认情况下,最左边的索引是变化最慢的索引,即在2D中是垂直的索引,因此正确的索引是arr[y][x]。这可以通过阵列大小的顺序来确认。其次,在图像,矩阵和numpy中,垂直尺寸通常表示为向下增加。这使您的邻居编号与我的不同。如有必要,只需将垂直位移乘以-1。


编辑

这里是产生完全相同结果的替代实现。速度稍快,但是修改了数组:

arr[1:-1, 1:-1] = np.where(arr[1:-1, 1:-1] > threshold, arr[1:-1, 1:-1] * arr[ :-2,  :-2] * factor, arr[1:-1, 1:-1])
arr[1:-1, 1:-1] = np.where(arr[1:-1, 1:-1] > threshold, arr[1:-1, 1:-1] * arr[ :-2, 1:-1] * factor, arr[1:-1, 1:-1])
arr[1:-1, 1:-1] = np.where(arr[1:-1, 1:-1] > threshold, arr[1:-1, 1:-1] * arr[ :-2, 2:  ] * factor, arr[1:-1, 1:-1])
arr[1:-1, 1:-1] = np.where(arr[1:-1, 1:-1] > threshold, arr[1:-1, 1:-1] * arr[1:-1,  :-2] * factor, arr[1:-1, 1:-1])
arr[1:-1, 1:-1] = np.where(arr[1:-1, 1:-1] > threshold, arr[1:-1, 1:-1] * arr[1:-1, 2:  ] * factor, arr[1:-1, 1:-1])
arr[1:-1, 1:-1] = np.where(arr[1:-1, 1:-1] > threshold, arr[1:-1, 1:-1] * arr[2:  ,  :-2] * factor, arr[1:-1, 1:-1])
arr[1:-1, 1:-1] = np.where(arr[1:-1, 1:-1] > threshold, arr[1:-1, 1:-1] * arr[2:  , 1:-1] * factor, arr[1:-1, 1:-1])
arr[1:-1, 1:-1] = np.where(arr[1:-1, 1:-1] > threshold, arr[1:-1, 1:-1] * arr[2:  , 2:  ] * factor, arr[1:-1, 1:-1])

答案 4 :(得分:1)

此答案假设您真的想要完全按照您在问题中所写的内容做。好吧,几乎完全正确,因为您的代码由于索引超出范围而崩溃。最简单的解决方法是添加条件,例如

if x > 0 and y < y_max:
    arr[x-1][y+1] = ...

不能使用numpy或scipy对主要操作 进行矢量化的原因是,所有已被“精简”的邻近细胞“还原”了所有细胞。大块或肮脏会在每个操作中使用邻居的不受影响的值。在我的另一个答案中,我演示了如果允许您按照8个步骤将操作分组,每个步骤沿一个特定邻居的方向进行分组,但是每个操作都在该步骤中使用 unaffected 值,则如何使用numpy进行操作邻居。就像我说的,这里我假设您必须按顺序进行。

在继续之前,让我在您的代码中交换xy。您的阵列具有典型的屏幕尺寸,其中高度为720,宽度为1440。图像通常按行存储,默认情况下,ndarray中最右边的索引是变化更快的索引,因此一切都说得通。公认这是违反直觉的,但正确的索引是arr[y, x]

可以对您的代码进行的主要优化(在我的Mac上将执行时间从〜9 s缩短为〜3.9 s)是在不需要时不为其分配单元格,并加上in-place multiplication使用[y, x]而不是[y][x]索引的。像这样:

y_size, x_size = arr.shape
y_max, x_max = y_size - 1, x_size - 1
for (y, x), value in np.ndenumerate(arr):
    reduce_by = value * 0.1
    if y > 0 and x < x_max:
        if arr[y - 1, x + 1] > 0.25: arr[y - 1, x + 1] *= reduce_by
    if x < x_max:
        if arr[y    , x + 1] > 0.25: arr[y    , x + 1] *= reduce_by
    if y < y_max and x < x_max:
        if arr[y + 1, x + 1] > 0.25: arr[y + 1, x + 1] *= reduce_by
    if y > 0:
        if arr[y - 1, x    ] > 0.25: arr[y - 1, x    ] *= reduce_by
    if y < y_max:
        if arr[y + 1, x    ] > 0.25: arr[y + 1, x    ] *= reduce_by
    if y > 0 and x > 0:
        if arr[y - 1, x - 1] > 0.25: arr[y - 1, x - 1] *= reduce_by
    if x > 0:
        if arr[y    , x - 1] > 0.25: arr[y    , x - 1] *= reduce_by
    if y < y_max and x > 0:
        if arr[y + 1, x - 1] > 0.25: arr[y + 1, x - 1] *= reduce_by

另一种优化(在我的Mac上使执行时间进一步缩短至〜3.0 s)是通过使用带有额外边界单元的数组来避免边界检查。我们不在乎边界包含什么值,因为它将永远不会被使用。这是代码:

y_size, x_size = arr.shape
arr1 = np.empty((y_size + 2, x_size + 2))
arr1[1:-1, 1:-1] = arr
for y in range(1, y_size + 1):
    for x in range(1, x_size + 1):
        reduce_by = arr1[y, x] * 0.1
        if arr1[y - 1, x + 1] > 0.25: arr1[y - 1, x + 1] *= reduce_by
        if arr1[y    , x + 1] > 0.25: arr1[y    , x + 1] *= reduce_by
        if arr1[y + 1, x + 1] > 0.25: arr1[y + 1, x + 1] *= reduce_by
        if arr1[y - 1, x    ] > 0.25: arr1[y - 1, x    ] *= reduce_by
        if arr1[y + 1, x    ] > 0.25: arr1[y + 1, x    ] *= reduce_by
        if arr1[y - 1, x - 1] > 0.25: arr1[y - 1, x - 1] *= reduce_by
        if arr1[y    , x - 1] > 0.25: arr1[y    , x - 1] *= reduce_by
        if arr1[y + 1, x - 1] > 0.25: arr1[y + 1, x - 1] *= reduce_by
arr = arr1[1:-1, 1:-1]

对于记录,如果可以使用numpy或scipy对这些操作进行矢量化处理,则该解决方案的速度至少会提高35倍(在我的Mac上测得)。

N.B .:如果依次对数组切片执行numpy did 操作,则以下代码会产生阶乘(即,正整数乘以一个数的乘积)–但不会:

>>> import numpy as np
>>> arr = np.arange(1, 11)
>>> arr
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10])
>>> arr[1:] *= arr[:-1]
>>> arr
array([ 1,  2,  6, 12, 20, 30, 42, 56, 72, 90])

答案 5 :(得分:0)

尝试使用熊猫

import pandas as pd
# create random array as pandas DataFrame
df = pd.DataFrame(pd.np.random.rand(720, 1440))  
# define the centers location for each 9x9
Center_Locations = (df.index % 3 == 1,
                    df.columns.values % 3 == 1)
# new values for the centers, to be use later
df_center = df.iloc[Center_Locations] * 1.25
# change the df, include center
df = df * 0.9 
# replacing only the centers values   
df.iloc[Center_Locations] = df_center 

答案 6 :(得分:0)

我们可以使用线性索引来做到这一点。如前所述,您的实现取决于您如何遍历数组。因此,我假设我们要修复数组,计算出要乘以每个元素的内容,然后简单地应用乘法。所以我们遍历数组无关紧要。

每个元素的乘积是:

1 if a[i,j] < 0.25 else np.prod(neighbours_a*0.1)

因此,我们将首先遍历整个数组,获得每个元素的8个邻居,将它们乘以0.1 ^ 8的因子,然后将这些值与a进行有条件的元素相乘。

为此,我们将使用线性索引并将其偏移。因此,对于具有m行,n列的数组,第i,jth个元素的线性索引为i n + j。要向下移动一行,我们只需将n添加为(i + 1),第j个元素的线性索引为(i + 1)n + j =(i n + j)+ n。这种算术运算法提供了一种获取每个点的邻居的好方法,因为邻居都是相对于每个点的固定偏移量。

import numpy as np

# make some random array
columns = 3
rows = 3
a = np.random.random([rows, columns])

# this contains all the reduce by values, as well as padding values of 1.
# on the top, bot left and right. we pad the array so we dont have to worry 
# about edge cases, when gathering neighbours. 
pad_row, pad_col = [1, 1], [1,1]
reduce_by = np.pad(a*0.1, [pad_row, pad_col], 'constant', constant_values=1.)

# build linear indices into the [row + 2, column + 2] array. 
pad_offset = 1
linear_inds_col = np.arange(pad_offset, columns + pad_offset)
linear_row_offsets = np.arange(pad_offset, rows + pad_offset)*(columns + 2*pad_offset)
linear_inds_for_array = linear_inds_col[None, :] + linear_row_offsets[:, None]

# get all posible row, col offsets, as linear offsets. We start by making
# normal indices eg. [-1, 1] up 1 row, along 1 col, then make these into single
# linear offsets such as -1*(columns + 2) + 1 for the [-1, 1] example
offsets = np.array(np.meshgrid([1, -1, 0], [1, -1, 0])).T.reshape([-1, 2])[:-1, :]
offsets[:,0] *= (columns + 2*pad_offset)
offsets = offsets.sum(axis=1)

# to every element in the flat linear indices we made, we just have to add
# the corresponding linear offsets, to get the neighbours
linear_inds_for_neighbours = linear_inds_for_array[:,:,None] + offsets[None,None,:]

# we can take these values from reduce by and multiply along the channels
# then the resulting [rows, columns] matrix will contain the potential
# total multiplicative factor to reduce by (if a[i,j] > 0.25)
relavent_values = np.take(reduce_by, linear_inds_for_neighbours)
reduce_by = np.prod(relavent_values, axis=2)

# do reduction
val_numpy = np.where(a > 0.25, a*reduce_by, a)

# check same as loop
val_loop = np.copy(a)
for i in range(rows):
    for j in range(columns):
        reduce_by = a[i,j]*0.1
        for off_row in range(-1, 2):
            for off_col in range(-1, 2):
                if off_row == 0 and off_col == 0:
                    continue
                if 0 <= (i + off_row) <= rows - 1 and 0 <= (j + off_col) <= columns - 1:
                    mult = reduce_by if a[i + off_row, j + off_col] > 0.25 else 1.
                    val_loop[i + off_row, j + off_col] *= mult


print('a')
print(a)
print('reduced np')
print(val_numpy)
print('reduce loop')
print(val_loop)
print('equal {}'.format(np.allclose(val_numpy, val_loop)))

答案 7 :(得分:0)

无法避免循环,因为减少是顺序执行的,而不是并行执行的。

这是我的实现。对于每个(i,j),以a为中心,创建a[i,j]的3x3块视图(由于我不想将其值临时设置为0,因此该值低于阈值,减少它)。对于边界处的(i,j),块在拐角处为2x2,在其他地方为2x3或3x2。然后,该块被阈值掩盖,未掩盖的元素乘以a_ij*0.1

def reduce(a, threshold=0.25, r=0.1):
    for (i, j), a_ij in np.ndenumerate(a):
        a[i,j] = 0       
        block = a[0 if i == 0 else (i-1):i+2, 0 if j == 0 else (j-1):j+2]   
        np.putmask(block, block>threshold, block*a_ij*r)  
        a[i,j] = a_ij   
    return a

请注意,还可以从边界单元周围的边界单元上进行归约,即循环从具有3个邻居的数组a[0, 0]的第一个角开始:a[0,1],{ {1}}和a[1,0],如果它们大于0.25,则会减少a[1,1]。然后转到具有5个邻居等的单元格a[0,0]*0.1。如果要严格操作具有8个邻居的单元格,即窗口大小为3x3,则循环应从a[0,1]到{{1 }},并且该函数应进行如下修改:

a[1,1]

示例:

a[-2, -2]

3x2数组的另一个示例:

def reduce_(a, threshold=0.25, r=0.1):
    ''' without borders -- as in OP's solution'''
    for (i, j), a_ij in np.ndenumerate(a[1:-1,1:-1]):
        block = a[i:i+3, j:j+3]
        mask = ~np.diag([False, True, False])*(block > threshold)
        np.putmask(block, mask, block*a_ij*r)   
    return a

答案 8 :(得分:-1)

通过对较小的问题进行分析,我们可以看到@jakevdp解决方案确实可以完成任务,但是却忘记了对mask<0.25 与掩码进行卷积之后是否进行检查,以便获得一些值可能稍后下降到0.25(每个像素可能有8个测试),因此必须有一个for循环,除非有一个我没有听说过的内置函数。

这是我的建议:

# x or y first depends if u want rows or cols , .. different results
for x in range(arr.shape[1]-3):
    for y in range(arr.shape[0]-3):
        k = arr[y:y+3,x:x+3]
        arr[y:y+3,x:x+3] = k/10**(k>0.25)