In python, using the cv2 library; how can I tile one image into a second image?

Viewed 619
import cv2
tile = cv2.imread('.\tile_pattern.png')
h,w = tile.shape[:2]
photo = cv2.imread('.\picture.png')
# ???? do something to copy tile onto photo @ x=10 y=10
# COPYIMAGE( tile, 0:0, h:w, photo, 10:10)
cv2.imshow('image', photo)
cv2.imwrite('./modified.png', photo)
cv2.waitKey(0)
cv2.destroyAllWindows()

How do I take the tile_pattern.png image and paste that into picture.png and save the combined image to modified.png?

Given that tile_pattern.png is a 16x16 color image; and picture.png is a 128x128 color image and I want to place tile_pattern.png on top of picture.png starting with the offset location X=10 and Y=10 within picture.png

2 Answers

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:

enter image description here

tile_pattern.png:

enter image description here

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:

enter image description here


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.

Here is a brute force solution that uses the more direct np.tile() method and masking to help tile the one image over the other in Python/OpenCV. It is not as elegant as the solution from @Ann Zen, but perhaps is easier to understand. I will use her images and parameters.

Image:

enter image description here

Tile:

enter image description here

import cv2
import numpy as np
import math

# read image
photo = cv2.imread('gray_gradient.png')
ph, pw = photo.shape[:2]

# read tile
tile = cv2.imread('red_green_tile.png')

# pad tile on right and bottom by 10 with black
top=0
bottom=10
left=0
right=10
tile_pad = cv2.copyMakeBorder(tile, top, bottom, left, right, cv2.BORDER_CONSTANT, (0,0,0))
tph, tpw = tile_pad.shape[:2]

# create white image the same size as tile
white = np.full_like(tile, (255,255,255))

# pad white image with black by 10 on right and bottom
white = cv2.copyMakeBorder(white, top, bottom, left, right, cv2.BORDER_CONSTANT, (0,0,0))

# tile the tile and white images out to be as larger or slightly larger than the input image and crop to same size
xrepeats = math.ceil(pw/tpw)
yrepeats = math.ceil(ph/tph)
print(yrepeats,xrepeats)
tiled_tile = np.tile(tile_pad, (yrepeats,xrepeats,1))[0:ph, 0:pw]
tiled_white = np.tile(white, (yrepeats,xrepeats,1))[0:ph, 0:pw]
tiled_white = tiled_white[:,:,0]

# mask the tile with the tiled_white image
masked_tile = cv2.bitwise_and(tiled_tile, tiled_tile, mask=tiled_white)

# mask the photo with the inverse tiled_white image
masked_photo = cv2.bitwise_and(photo, photo, mask=255-tiled_white)

# combine the two masked images
result = cv2.add(masked_photo, masked_tile)

# save results
cv2.imwrite("red_green_tile_tiled.png", tiled_tile)
cv2.imwrite("red_green_tile_tiled_mask.png", tiled_white)
cv2.imwrite("red_green_tile_tiled_over_gray_gradient.png", result)

# show the results
cv2.imshow("tiled_tile", tiled_tile)
cv2.imshow("tiled_white", tiled_white)
cv2.imshow("masked_tile", masked_tile)
cv2.imshow("masked_photo", masked_photo)
cv2.imshow("result", result)
cv2.waitKey(0)

Tiled out tile on black:

enter image description here

Mask:

enter image description here

Result:

enter image description here

Related