You can use the np.lib.stride_tricks.as_strided() method, which basically allows you to create an array of sliding windows through an array. Lets say we have these 2 images:
picture.png:

tile_pattern.png:

Here is how you might utilize the said method:
import cv2
import numpy as np
tile = cv2.imread('./tile_pattern.png')
photo = cv2.imread('./picture.png')
x_off = 10
y_off = 10
p_h, p_w, _ = photo.shape
t_h, t_w, _ = tile.shape
shape = (p_h // (t_h - 1 + y_off),
p_w // (t_w - 1 + x_off), t_h, t_w, 3)
strides = ((t_h + y_off) * 3 * p_w,
(t_w + x_off) * 3, p_w * 3, 3, 1)
np.lib.stride_tricks.as_strided(photo, shape, strides)[:] = tile
cv2.imshow('photo', photo)
cv2.imwrite('./modified.png', photo)
cv2.waitKey(0)
cv2.destroyAllWindows()
Output:

Explanation of the parameters of the np.lib.stride_tricks.as_strided() method:
The np.lib.stride_tricks.as_strided() method takes in one positional argument, x: an array, and a few keyword arguments. The 2 keyword arguments I used in the above code are shape: the shape of the array the method should return, and strides.
The strides keyword argument can be confusing, so lets have a look at a few examples:
>>> import numpy as np
>>> arr = np.array([4, 9, 3, 11])
>>> arr.strides
(8,)
The 8 represents the number of bytes it takes to move from the first element to the next element in the array. So lets try:
>>> np.lib.stride_tricks.as_strided(arr, (4,), (8,))
array([ 4, 9, 3, 11])
So the above line of code takes in the arr array, returns an array of shape (4,), and it moves 8 bytes to get to every other element. Now lets try:
>>> np.lib.stride_tricks.as_strided(arr, (4,), (4,))
array([ 4, 38654705664, 9, 12884901888])
Instead of telling the method to move 8 bytes to get every other element, we used 4. If you recall from arr.strides, it already indicated that every element is 8 bytes apart, hence 4 will make it so that every other 2 elements in the resulting array will be from the consecutive elements in the original array. The numbers you see between the 2 numbers are random pieces of memory that doesn't belong in arr.
We can already create a sliding window from arr, like this:
>>> np.lib.stride_tricks.as_strided(arr, (3, 2), (8, 8))
array([[ 4, 9],
[ 9, 3],
[ 3, 11]])
The first 8 in the strides keyword argument specifies that the start of each row in the resulting array should have the element in arr that is 8 bytes away from every other row's starting element, and the second 8 specifies that every other element in each row should be 8 bytes apart from arr.
The reason you don't see 8 in my strides in the tiling-image code is because the strides of arrays of type uint8 are 1, not 8. See:
>>> import numpy as np
>>> arr = np.array([4, 9, 3, 11], 'uint8')
>>> arr.strides
(1,)
Also, if you're wondering about the * 3s, since the image has 3 channels, we'll need to move 3 bytes to get from one pixel to the other.
Further explanation on the parameters of the np.lib.stride_tricks.as_strided() method:
Lets have a look at how we can determine the shape and strides of the sliding window we'll need.
shape = (p_h // (t_h - 1 + y_off),
p_w // (t_w - 1 + x_off), t_h, t_w, 3)
strides = ((t_h + y_off) * 3 * p_w,
(t_w + x_off) * 3, p_w * 3, 3, 1)
We can see that the shape of the resulting array we want from the np.lib.stride_tricks.as_strided() method should have 4 dimensions:
- one for each row of tiles
- one for each tile
- one for each row in each tile
- one for each 3 channel pixel
So the first value in the shape keyword argument is the number of rows of tiles we are expecting to tile onto the image (including the y-offset), and the second value is the number of tiles per row (including the x-offset). The third value is the number of rows per tile, the forth value is the number of pixels per row of each tile and the last value is the number of channels per pixel.
Keeping in mind that each adjacent element in a uint8 array is one byte apart, we won't have to worry about multiplying the strides by the strides of the array, but do note that it's safer to utilize the values in img.strides anyway.
The first value in the strides keyword argument is the number of elements it takes to go from the first element in the original array to the element in the original array that will become the first element of the resulting array's second row.
The second value in the strides keyword argument is the number of elements it takes to go from the first element in the original array to the element in the original array that will become the first element of the resulting array's second tile.
The third value in the strides keyword argument is the number of elements it takes to go from the first element in the original array to the element in the original array that will become the first element of the resulting array's first tile's second row.
The forth value in the strides keyword argument is the number of elements it takes to go from the first element in the original array to the element in the original array that will become the first element of the resulting array's first tile's first row's second pixel.
The last value in the strides keyword argument is the number of elements it takes to go from the first element in the original array to the element in the original array that will become the second element of the resulting array.
Again, every value would have to be multiplied by 8 if the array were of type, say, float64.