Search code examples
pythonopencvalpha-transparency

how to efficiently and correctly overlay pngs taking into account transparency?


when i was trying to overlay one image over the other one image had a transparent rounded rectangle filling and the other was just a normal image it looked either like this here ( just putting the yellow over the pink without taking into account the rounded corners at all) or like this here (looks just like the rounded rectangle without adding anything even kept the transparency)

this is how it should look like: correct output

here are the 2 example images: (pink.png) pink and (yellow.png) yellow

here is the code used for this :

import cv2
import numpy as np
layer0 = cv2.imread(r'yellow.png', cv2.IMREAD_UNCHANGED)
h0, w0 = layer0.shape[:2]

layer4 = cv2.imread(r"pink.png", cv2.IMREAD_UNCHANGED)

#just a way to help the image look more transparent in the opencv imshow because imshow always ignores
# the transparency and pretends that the image has no alpha channel
for y in range(layer4.shape[0]):
   for x in range(layer4.shape[1]):
      if layer4[y,x][3]<255:
         layer4[y,x][:] =0,0,0,0


# Create a new np array

shapes = np.zeros_like(layer4, np.uint8)
shapes = cv2.cvtColor(shapes, cv2.COLOR_BGR2BGRA)

#the start position of the yellow image on the pink
gridpos = (497,419)

shapes[gridpos[1]:gridpos[1]+h0, gridpos[0]:gridpos[0]+w0] = layer0


# Change this into bool to use it as mask

mask = shapes.astype(bool)


# We'll create a loop to change the alpha
# value i.e transparency of the overlay
for alpha in np.arange(0, 1.1, 0.1)[::-1]:

    # Create a copy of the image to work with
    bg_img = layer4.copy()
    # Create the overlay
    bg_img[mask] = cv2.addWeighted( bg_img,1-alpha, shapes, alpha, 0)[mask]

    # print the alpha value on the image
    cv2.putText(bg_img, f'Alpha: {round(alpha,1)}', (50, 200),
                cv2.FONT_HERSHEY_PLAIN, 8, (200, 200, 200), 7)

    # resize the image before displaying
    bg_img = cv2.resize(bg_img, (700, 600))
    cv2.imwrite("out.png", bg_img)
    cv2.imshow('Final Overlay', bg_img)

    cv2.waitKey(0)

you can test different alpha combinations by pressing a key on the keyboard


Solution

  • OpenCV Version

    Took me some time, but basically you have to mask both images and then combine them. The code bellow is commented and should be self explenatory. I think the hardest part to grasp is, that your pink image actually represents the foreground and the yellow image is your background. The trickiest part is to not let anything through from your background, which is why you have to mask both images.

    import cv2
    import numpy as np
    
    pink = cv2.imread("pink.png", cv2.IMREAD_UNCHANGED)
    # We now have to use an image that has the same size as the pink "foreground"
    # and create a black image wiht numpy's zeros_like (gives same size as input)
    background = np.zeros_like(pink)
    
    # We then split the pink image into 4 channels:
    # b, g, r and alpha, we only need the alpha as mask
    _, _, _, mask = cv2.split(pink)
    
    yellow = cv2.imread("yellow.png", cv2.IMREAD_UNCHANGED)
    # we need the x and y dimensions for pasting the image later
    h_yellow, w_yellow = yellow.shape[:2]
    
    
    # Assuming format is (x, y)
    gridpos = (497, 419)
    
    # We paste the yellow image onto our black background
    # IMPORTANT: if any of the dimensions of yellow plus the gridpos is
    # larger than the background width or height, this will give you an
    # error! Also, this only works with the same number of input channels.
    # If you are loading a jpg image without alpha channel, you can adjust
    # the number of channels, the last input param, e.g. with :3 to only use
    # the first 3 channels
    background[gridpos[1]:gridpos[1] + h_yellow, gridpos[0]:gridpos[0] + w_yellow, :] = yellow
    
    # This step was not intuitive for me in the first run, since the
    # pink img should aready be masked, but for some reason, it is not
    pink_masked = cv2.bitwise_and(pink, pink, mask=mask)
    
    # In this step, we mask the positioned yellow image with the inverse
    # mask from the pink image, achieved by bitwise_not
    background = cv2.bitwise_and(background, background, mask=cv2.bitwise_not(mask))
    
    # We combine the pink masked image with the background
    img = cv2.convertScaleAbs(pink_masked + background)
    
    cv2.imshow("img", img), cv2.waitKey(0), cv2.destroyAllWindows()
    

    Cheers!


    Old Answer:

    It looks like you are setting the whole image as a mask, this is why the rounded corners have no effect at all from your pink background. I myself was struggling a lot with this task aswell and ended up using pillow instead of OpenCV. I don't know if it is more performant, but I got it running.

    Here the code that works for your example:

    from PIL import Image
    
    # load images
    background = Image.open(r"pink.png")
    # load image and scale it to the same size as the background
    foreground = Image.open(r"yellow.png").resize(background.size)
    # split gives you the r, g, b and alpha channel of the image.
    # For the mask we only need alpha channel, indexed at 3
    mask = background.split()[3]
    
    # we combine the two images and provide the mask that is applied to the foreground.
    im = Image.composite(background, foreground, mask)
    im.show()
    

    If your background is not monochrome as in your example, and you want to use the version, where you paste your original image, you have to create an empty image with the same size as the background, then paste your foreground to the position (your gridpos), e.g. like this:

    canvas = Image.new('RGBA', background.size)
    canvas.paste(foreground, gridpos)
    foreground = canvas
    

    Hope this helps!