0

I am trying to create a function that imprints a smaller array onto another. The dimensions and center are arbitrary. For example I may want to put a 3x3 on a 5x5 at center (1, 2) or I may want to put a 5x5 on a 100x100 at center (50, 30). Ignoring indexing errors and even arrays that have no center.

Example:
arr1 = 
[2, 3, 5]
[1, 5, 6]
[1, 0, 1]

arr2 = 
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]

arr3 = imprintarray(arr1, arr2, (1, 2))

arr3 =
[0, 2, 3, 5, 0]
[0, 1, 5, 6, 0]
[0, 1, 0, 1, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]

I call the first array the smallarray (the one that is imprinted) and the second array to be the map array (the bigger array who has its values modified)

My solution was to create a 3rd array with the target indexes on the the maparray and to iterate through accessing the smallarrays values and changing the elements of the maparray directly.

import numpy as np

maparray = np.zeros((9, 9))
smallarray = np.zeros((3, 3))
smallarray[:] = 2


def createindexarray(dimensions, center):
    array = []
    adjustment = (dimensions[0] * -0.5) + 0.5
    for row in range(dimensions[0]):
        elements = []
        for col in range(dimensions[1]):
            elements.append((row + center[0] + int(adjustment), col + center[1] + int(adjustment)))
        array.append(elements)
    return array


indexarray = createindexarray((3, 3), (3, 5))

for w, x in enumerate(smallarray):
    for y, z in enumerate(x):
        maparray[indexarray[w][y][0]][indexarray[w][y][1]] = smallarray[w][y]

It does feel like there should be a better way or more efficient way. I looked through numpy's documentation to see if I could find something like this but I could not find it. Thoughts? Even if you think this is the best way any tips on improving my Python would be much appreciated.

mf7aXtUs
  • 3
  • 1
  • You know the existence of slices, why do you need to assign values through iteration? – Mechanic Pig Sep 04 '22 at 03:08
  • I think you basically want https://stackoverflow.com/questions/40690248/copy-numpy-array-into-part-of-another-array, except you need to also check whether the indices are out-of-bounds. – Adam Oppenheimer Sep 04 '22 at 03:10
  • @MechanicPig Can you show what you mean (even just in pseudo code)? I think the hard part is the centering part, and I can't see how you match the smallarrays values to the maparray without iterating through while knowing where you are trying to place each element. – mf7aXtUs Sep 04 '22 at 03:21
  • @AdamOppenheimer It is close but I do not want an evenly padded array, the placement of where the values go is arbitrary, not directly in the center of the new array. I don't think padding works here. – mf7aXtUs Sep 04 '22 at 03:24
  • @mf7aXtUs the first answer in the post I linked demonstrates array slicing – Adam Oppenheimer Sep 04 '22 at 03:24
  • You two are right, I tried it with slicing. I don't quite see how to center it, but slicing is definitely the way to go. Thanks. (I am very new to programming so I honestly didnt really consider slicing) – mf7aXtUs Sep 04 '22 at 03:33

5 Answers5

1

Here is my adjustment to @Shahab Rahnama's solution that accounts for @Mechanic Pig's comment about the shift only working for (3x3) matrices, and my comment about the code not working if the matrix is placed with some elements out of bounds.

import numpy as np

def imprint(center, bigarray, smallarray):
    half_height = smallarray.shape[0] // 2
    half_width = smallarray.shape[1] // 2
    top = center[0] - half_height
    bottom = center[0] + half_height + 1
    left = center[1] - half_width
    right = center[1] + half_width + 1
    bigarray[ \
        max(0, top): \
            min(bigarray.shape[0], bottom),
        max(0, left): \
            min(bigarray.shape[1], right)
    ] = smallarray[ \
            (top < 0) * abs(top): \
                smallarray.shape[0] - (bottom > bigarray.shape[0]) * (bottom - bigarray.shape[0]), \
            (left < 0) * abs(left): \
                smallarray.shape[1] - (right > bigarray.shape[1]) * (right - bigarray.shape[1]) \
    ]
    return bigarray

bigarray = np.zeros((7, 7))
smallarray = 2 * np.ones((5, 5))

imprint((3, 3), bigarray, smallarray)
1

Create index arrays using np.meshgrid and the smallarray's shape.

bigarray = np.zeros((9, 9))
smallarray = np.zeros((3, 3)) + 2
x,y = smallarray.shape
xv,yv = np.meshgrid(np.arange(x),np.arange(y))
#print(xv)
#print(yv)

Those can be used on the left-hand-side of an assignment to imprint the smallarray. Without modification the smallarray will be positioned in the upper-left corner.

bigarray[xv,yv] = smallarray
print(bigarray)

[[2. 2. 2. 0. 0. 0. 0. 0. 0.]
 [2. 2. 2. 0. 0. 0. 0. 0. 0.]
 [2. 2. 2. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]]

The position can be changed by adding a scalar to the index arrays.

bigarray[xv+2,yv+3] = smallarray + 4
print(bigarray)

[[2. 2. 2. 0. 0. 0. 0. 0. 0.]
 [2. 2. 2. 0. 0. 0. 0. 0. 0.]
 [2. 2. 2. 6. 6. 6. 0. 0. 0.]
 [0. 0. 0. 6. 6. 6. 0. 0. 0.]
 [0. 0. 0. 6. 6. 6. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]]

The scalars will be the index in the bigarray where the [0,0] point of the smallarray will be placed. If the function needs to accept the position of the center of the smallarray just use its shape to calculate the appropriate scalar. Also use both arrays' shape to calculate whether the small one will fit then adjust.


The index arrays can also be made with .indices.

xv,yv = np.indices(smallarray.shape)
wwii
  • 23,232
  • 7
  • 37
  • 77
0

Yes, there is a better way :

import numpy as np

maparray = np.zeros((9, 9))
smallarray = np.zeros((3, 3))
smallarray[:] = 2


def imprint(center, maparray, smallarray):
    maparray[center[0]:center[0]+smallarray.shape[0],center[1]:center[1]+smallarray.shape[1]] = smallarray
    return maparray


print(imprint((3, 5), maparray, smallarray))

I used roll to overcome out-of-bounds (if the center is on the edge like [7,7] and [4,7], the smallarray will be out-of-bounds):

import numpy as np

maparray = np.zeros((9, 9))
smallarray = np.zeros((3, 3))
smallarray[:] = 2


def imprint(center, maparray, smallarray):
    rollx= 0
    rolly= 0
    if maparray.shape[0] - center[0] < smallarray.shape[0]:
        rollx = maparray.shape[0] - center[0] - smallarray.shape[0]
        center[0] = center[0] - abs(rollx )
    if maparray.shape[1] - center[1] < smallarray.shape[1]:
        rolly = maparray.shape[1] - center[1] - smallarray.shape[1]
        center[1] = center[1] - abs(rolly )
    
    maparray[center[0]:center[0]+smallarray.shape[0],center[1]:center[1]+smallarray.shape[1]] = smallarray
    return np.roll(maparray, (abs(rollx ), abs(rolly )), axis=(0, 1))


print(imprint([7, 7], maparray, smallarray))

Output:

[[2. 0. 0. 0. 0. 0. 0. 2. 2.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [2. 0. 0. 0. 0. 0. 0. 2. 2.]
 [2. 0. 0. 0. 0. 0. 0. 2. 2.]]
Shahab Rahnama
  • 982
  • 1
  • 7
  • 14
  • This doesn't work if the small array is placed such that some of the elements will be out-of-bounds, which was stated as one of the conditions for the requested function. – Adam Oppenheimer Sep 04 '22 at 03:49
  • 1
    The -1 here is rather lax, which is only valid for matrices with (3, 3) shape. – Mechanic Pig Sep 04 '22 at 03:50
  • @AdamOppenheimer yes, you can check shapes in an if...else statement and split it to what you want. – Shahab Rahnama Sep 04 '22 at 03:52
  • @MechanicPig I don't take your point. I add it BCZ array starts from 0 – Shahab Rahnama Sep 04 '22 at 03:54
  • Try `imprint((3, 3), np.zeros((7, 7)), np.ones((5, 5)))`, does it place 1 in the range of the middle (5,5) of the matrix? A simpler example is `imprint((1, 1), np.zeros((3, 3)), np.ones((1, 1)))`, which places 1 in the upper left corner of the matrix instead of the center. – Mechanic Pig Sep 04 '22 at 04:01
  • @MechanicPig Updated :) – Shahab Rahnama Sep 04 '22 at 04:13
  • 1
    @AdamOppenheimer I don't see where such a condition is stated. – Karl Knechtel Sep 04 '22 at 04:26
  • @KarlKnechtel Actually I think you are right, I misread "Ignoring indexing errors and even arrays that have no center." as meaning mf7aXtUs wanted to allow matrices to go out-of-bounds, and indexing could be ignored, but maybe he/she/they just meant don't worry about checking that the center is inside the array. – Adam Oppenheimer Sep 04 '22 at 04:28
  • @AdamOppenheimer For my specific case I don't want wrapping, I planned on catching it with a Try except block. Wrapping would be incorrect for my case but it is nice that people included it in their solutions. – mf7aXtUs Sep 04 '22 at 05:21
0

Ignoring your requirement for "center" alignment1, let's say you want to place the values of some array a1 into another array a2 with some offset offset. offset is the coordinate in a2 where a1[0, 0, ..., 0] ends up. The values can be negative without wrapping.

The sizes of the arrays are irrelevant. We only need to assume that the types are at least somewhat compatible, and that a1.ndim == a2.ndim == len(offset). Let's call the number of dimensions d.

The goal is to be able to create a tuple of slices for each array, so that the assignment becomes a simple indexing operation:

a2[index2] = a1[index1]

So let's start writing this function:

def emplace(a1, a2, offset):
    offset = np.asanyarray(offset)
    if a1.ndim != a2.ndim or a1.ndim != len(offset) or len(offset) != offset.size:
        raise ValueError('All dimensions must match')

First let's filter out the cases where there is no overlap at all:

    if a1.size == 0 or a2.size == 0:
        return
    if (offset + a1.shape <= 0).any() or (offset >= a2.shape).any():
        return

Now that you know that there will be overlap, you can compute the starting indices for both arrays. If offset is negative, that truncates leading elements from a1. If it's positive, it skips elements in a2.

    start1 = np.where(offset < 0, -offset, 0)
    start2 = np.where(offset < 0, 0, -offset)

The ends can be computed in a similar manner, except that the bound check is done on the opposite end now:

    stop1 = np.where(offset + a1.shape > a2.shape, a2.shape - offset, a1.shape)
    stop2 = np.where(offset + a1.shape > a2.shape, a2.shape, offset + a1.shape)

You can construct tuples of indices from the bounds:

    index1 = tuple(slice(*bounds) for bounds in np.stack((start1, stop1), axis=-1))
    index2 = tuple(slice(*bounds) for bounds in np.stack((start2, stop2), axis=-1))

And so the final emplacement becomes just a simple assignment:

    a2[index2] = a1[index1]

TL;DR

Here is how I would write the full function:

def emplace(a1, a2, offset):
    offset = np.asanyarray(offset)

    if a1.ndim != a2.ndim or a1.ndim != len(offset) or len(offset) != offset.size:
        raise ValueError('All dimensions must match')

    if a1.size == 0 or a2.size == 0:
        return
    if (offset + a1.shape <= 0).any() or (offset >= a2.shape).any():
        return

    noffset = -offset
    omask = offset < 0
    start1 = np.where(omask, noffset, 0)
    start2 = np.where(omask, 0, noffset)

    omask = offset + a1.shape > a2.shape
    stop1 = np.where(omask, a2.shape - offset, a1.shape)
    stop2 = np.where(omask, a2.shape, offset + a1.shape)

    index1 = tuple(slice(*bounds) for bounds in np.stack((start1, stop1), axis=-1))
    index2 = tuple(slice(*bounds) for bounds in np.stack((start2, stop2), axis=-1))
    a2[index2] = a1[index1]

1 I'm not suggesting you actually ignore how you want to do things. Just convert the center alignment to a corner offset.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • Note: this function is pretty general and will work on arrays of any dimension. The bad part is that is written on mobile, completely untested, and I won't have the means to test it for at least a couple more days. – Mad Physicist Sep 04 '22 at 04:59
  • I can't seem to get this to work. I believe the line if (offset + a1.shape <= 0).any() or (offset >= a2.shape).any(): return is always going to return without doing the rest. (How the hell do I press enter without it auto posting?) – mf7aXtUs Sep 04 '22 at 05:52
-1

Given:

import numpy as np

maparray = np.zeros((9, 9))
smallarray = np.ones((3, 3)) * 2

We can simply assign to a slice:

# to assign with the top-left at (1, 2)
maparray[1:4, 2:5] = smallarray

which we can generalize:

def imprint(big, small, position):
    y, x = position
    h, w = small.shape
    big[y:y+h, x:x+w] = small

If we need to wrap around the outside, then we can take a different approach, still without needing any conditional logic. Simply roll the big array such that the "imprinting" location is at the top-left; assign the values; then roll back. Thus:

def imprint(big, small, position):
    y, x = position
    h, w = small.shape
    copy = np.roll(big, (-y, -x), (0, 1))
    copy[:h, :w] = small
    big[:, :] = np.roll(copy, (y, x), (0, 1))

Let's test it:

>>> imprint(maparray, smallarray, (7, 7))
>>> maparray
array([[2., 0., 0., 0., 0., 0., 0., 2., 2.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [2., 0., 0., 0., 0., 0., 0., 2., 2.],
       [2., 0., 0., 0., 0., 0., 0., 2., 2.]])
>>> imprint(maparray, smallarray, (2, 2))
>>> maparray
array([[2., 0., 0., 0., 0., 0., 0., 2., 2.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 2., 2., 2., 0., 0., 0., 0.],
       [0., 0., 2., 2., 2., 0., 0., 0., 0.],
       [0., 0., 2., 2., 2., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [2., 0., 0., 0., 0., 0., 0., 2., 2.],
       [2., 0., 0., 0., 0., 0., 0., 2., 2.]])

We see that we can correctly leave the results from previous imprints in place, and also wrap around the edge.

It also works with negative indices and large indices, wrapping around in the expected manner:

>>> maparray[:] = 0 # continuing from before
>>> imprint(maparray, smallarray, (-1, -1))
>>> maparray
array([[2., 2., 0., 0., 0., 0., 0., 0., 2.],
       [2., 2., 0., 0., 0., 0., 0., 0., 2.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [2., 2., 0., 0., 0., 0., 0., 0., 2.]])
>>> # Keep in mind the first coordinate is for the first axis, which
>>> # displays vertically here (hence labelling it `y` in the code).
>>> imprint(maparray, smallarray, (40, 0))
>>> maparray
array([[2., 2., 0., 0., 0., 0., 0., 0., 2.],
       [2., 2., 0., 0., 0., 0., 0., 0., 2.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [2., 2., 2., 0., 0., 0., 0., 0., 0.],
       [2., 2., 2., 0., 0., 0., 0., 0., 0.],
       [2., 2., 2., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [2., 2., 0., 0., 0., 0., 0., 0., 2.]])

It will presumably be more efficient to use conditional logic explicitly (left as an exercise here), but this approach is elegant and easy to understand.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153