Search code examples
pythonnumpyscipy

NumPy way to modify 2d array by 2×2 blocks


I am iterating and modifying 2×2 blocks on a 2d grid. What is a numpy/scipy way to do this more efficiently than with basic python cycling?

transitions = {b'\x00\x00\x00\x00': np.array([True, True, True, True]), …}
for y, x in itertools.product(range(yshift, ny, 2), range(xshift, nx, 2)):
    grid[y:y+2, x:x+2].flat = transitions[grid[y:y+2, x:x+2].tobytes()]

Solution

  • One example, starting from a lookup table using directly the shape of the subarrays you want to change

    lut=np.zeros((2,2,2,2,2,2)) # 4 1st "2" are the booleans 0/1 values for the 4 2x2 values. And the two last 2,2 are the 2x2 result to replace them with
    lut[0,0,0,0]=[[1,1],[1,1]] # Translated from your sole example
    lut[0,0,0,1]=[[1,1],[1,0]]
    lut[0,0,1,0]=[[1,1],[0,1]]
    lut[1,1,0,0]=[[1,1],[1,1]] # Just some random examples. Rest can stay 0
    
    A=np.random.randint(0,2,(10,10)) # just 10x10 0/1 values
    
    # Here starts the trick
    Av=np.lib.stride_tricks.as_strided(A, shape=(5,5,2,2), strides=(A.strides[0]*2, A.strides[1]*2, A.strides[0], A.strides[1]))
    # Av is another view of A, seen as a 5x5 array of 2x2 arrays
    # Could have done that by "reshape". Just, want to be sure it is a view
    # (so no copy of data. And what ever I do to Av, is done on A)
    Av[:] = lut[Av[...,0,0], Av[...,0,1],Av[...,1,0], Av[...,1,1]]
    
    
    

    Example Result

    #Before
    [[0 1 0 1 1 1 1 1 1 0]
     [0 1 1 1 1 1 1 1 0 0]
     [0 0 1 1 0 1 0 1 1 0]
     [0 0 0 1 0 1 1 0 1 1]
     [0 1 1 1 0 1 1 1 0 0]
     [0 0 0 0 0 1 1 0 0 0]
     [1 0 1 1 1 0 1 0 0 0]
     [0 1 1 0 1 0 0 0 1 0]
     [1 1 0 1 0 1 0 1 1 0]
     [0 1 1 1 1 1 1 1 1 1]]
    
    # After
    [[0 0 0 0 0 0 0 0 0 0]
     [0 0 0 0 0 0 0 0 0 0]
     [1 1 0 0 0 0 0 0 0 0]
     [1 1 0 0 0 0 0 0 0 0]
     [0 0 1 1 0 0 0 0 1 1]
     [0 0 1 1 0 0 0 0 1 1]
     [0 0 0 0 0 0 0 0 1 1]
     [0 0 0 0 0 0 0 0 0 1]
     [0 0 0 0 0 0 0 0 0 0]
     [0 0 0 0 0 0 0 0 0 0]]
    

    Of course, this can be done only a part of A Av[2:4,2:4] = lut[Av[2:4,2:4,0,0], Av[2:4,2:4,0,1], Av[2:4,2:4,1,0], Av[2:4,2:4,1,1]]

    To avoid having to repeat the four Av in the fancy indexing, we can recycle a bit your intial idea (not faster. And need to make asumptions on dtype, which I don't. Plus, I am pretty sure that some numpy fancy indexing gurus could find a way to do it in pure numpy indexing), we can translate the 4 1/0 into a 0-15 index

    lut2=np.zeros((16,2,2))
    lut2[0]=[[1,1],[1,1]] # Translated from your sole example
    lut2[1]=[[1,1],[1,0]]
    lut2[2]=[[1,1],[0,1]]
    lut2[12]=[[1,1],[1,1]] # Just some random examples. Rest can stay 0
    
    Av=np.lib.stride_tricks.as_strided(A, shape=(5,5,2,2), strides=(A.strides[0]*2, A.strides[1]*2, A.strides[0], A.strides[1]))
    
    # 0 to 15 index - it is tempting to try to do it with to_bytes or the like. But the blocks are not contiguous, even
    # with a strides, data.
    idx = (Av*[[8,4],[2,1]]).sum(axis=(2,3))
    Av[:] = lut2[idx]