Using openCV to overlay transparent image onto another image
Asked Answered
H

10

71

How can I overlay a transparent PNG onto another image without losing it's transparency using openCV in python?

import cv2

background = cv2.imread('field.jpg')
overlay = cv2.imread('dice.png')

# Help please

cv2.imwrite('combined.png', background)

Desired output: enter image description here

Sources:

Background Image

Overlay

Hypocrite answered 30/11, 2016 at 18:38 Comment(6)
Same as here, but in C++. You should be able to port to Python without much effortPetiolule
@Petiolule I'm a PHP guy and I'm not very familiar with C++ (or Python)Hypocrite
Here is a Python version.Similar
hi @Miki, i tried your code and the code on another question with given images. second code gives desired result perfectly.Broil
Not sure about python version, but on C++ you can first cvtColor your background to RGBA (4channels) and make sure both images are of the same size, then you can simply do a matrix add operation result = background + overlayCalycle
Easy example here... https://mcmap.net/q/276379/-how-to-overlay-a-png-on-a-webcam-feedIndifferentism
E
41
import cv2

background = cv2.imread('field.jpg')
overlay = cv2.imread('dice.png')

added_image = cv2.addWeighted(background,0.4,overlay,0.1,0)

cv2.imwrite('combined.png', added_image)

added_image

Editor answered 13/9, 2017 at 13:29 Comment(4)
How about the case that I want both layer to be of alpha = 1? e.g., i have a foreground with a ball on it, transparent background. If this foreground is overlay on a solid blue background, the ball part should be masked out. how can i do that?Noahnoak
@Noahnoak #46104231Kinetics
this answer doesn't use the alpha channel of the overlayJarred
the code of this answer will also throw an error because both pictures aren't of the same size, but cv.addWeighted requires that.Jarred
I
43

The correct answer to this was far too hard to come by, so I'm posting this answer even though the question is really old. What you are looking for is "over" compositing, and the algorithm for this can be found on Wikipedia: https://en.wikipedia.org/wiki/Alpha_compositing

I am far from an expert with OpenCV, but after some experimentation this is the most efficient way I have found to accomplish the task:

import cv2

background = cv2.imread("background.png", cv2.IMREAD_UNCHANGED)
foreground = cv2.imread("overlay.png", cv2.IMREAD_UNCHANGED)

# normalize alpha channels from 0-255 to 0-1
alpha_background = background[:,:,3] / 255.0
alpha_foreground = foreground[:,:,3] / 255.0

# set adjusted colors
for color in range(0, 3):
    background[:,:,color] = alpha_foreground * foreground[:,:,color] + \
        alpha_background * background[:,:,color] * (1 - alpha_foreground)

# set adjusted alpha and denormalize back to 0-255
background[:,:,3] = (1 - (1 - alpha_foreground) * (1 - alpha_background)) * 255

# display the image
cv2.imshow("Composited image", background)
cv2.waitKey(0)
Illume answered 6/12, 2019 at 10:22 Comment(5)
almost. imshow is given background, which will be of type float64... but the values are in the range of 0..255, so the output will be blown out. either .astype(np.uint8) or divide by 255.Jarred
@MitchMcMabers If you know of an OpenCV built-in to perform "over compositing" then by all means please do post it as I have no doubt it would be significantly faster. But while addWeighted() may be a lot faster than the above code, but it is also not actually doing what the question is asking for.Illume
I agree with Mala, @MitchMcMabers. your comment is wrong. addWeighted does not perform per-element multiplication. it can't perform alpha blending. this answer is what's required. OpenCV currently has no builtins that do this in one step. numpy isn't slow. it's running compiled code behind most operations. it may be slower than numba-optimized code because its APIs deal with arbitrary dtypes.Jarred
` background[:,:,color] = alpha_foreground * foreground[:,:,color] + alpha_background * background[:,:,color] * (1 - alpha_foreground)` raises "ValueError: operands could not be broadcast together with shapes (500,500) (111,111)" (In my case (500,500) refers to the background image. I wonder hoe this solution obtained so many upvotes. Unlike @Ben's solution that follows, which works fine (using the same images) and which got half of the upvotes! Well, this is not the first time, I know.Associate
@Associate Having been posted 3 years earlier and having existed as an answer more than twice as long probably has something to do with it. At the time this answer was posted it was the only answer on this question that resolved it, hence the many upvotes. You are correct that this answer assumes both images to be of the same dimensions.Illume
E
41
import cv2

background = cv2.imread('field.jpg')
overlay = cv2.imread('dice.png')

added_image = cv2.addWeighted(background,0.4,overlay,0.1,0)

cv2.imwrite('combined.png', added_image)

added_image

Editor answered 13/9, 2017 at 13:29 Comment(4)
How about the case that I want both layer to be of alpha = 1? e.g., i have a foreground with a ball on it, transparent background. If this foreground is overlay on a solid blue background, the ball part should be masked out. how can i do that?Noahnoak
@Noahnoak #46104231Kinetics
this answer doesn't use the alpha channel of the overlayJarred
the code of this answer will also throw an error because both pictures aren't of the same size, but cv.addWeighted requires that.Jarred
S
29

If performance isn't a concern then you can iterate over each pixel of the overlay and apply it to the background. This isn't very efficient, but it does help to understand how to work with png's alpha layer.

slow version

import cv2

background = cv2.imread('field.jpg')
overlay = cv2.imread('dice.png', cv2.IMREAD_UNCHANGED)  # IMREAD_UNCHANGED => open image with the alpha channel

height, width = overlay.shape[:2]
for y in range(height):
    for x in range(width):
        overlay_color = overlay[y, x, :3]  # first three elements are color (RGB)
        overlay_alpha = overlay[y, x, 3] / 255  # 4th element is the alpha channel, convert from 0-255 to 0.0-1.0

        # get the color from the background image
        background_color = background[y, x]

        # combine the background color and the overlay color weighted by alpha
        composite_color = background_color * (1 - overlay_alpha) + overlay_color * overlay_alpha

        # update the background image in place
        background[y, x] = composite_color

cv2.imwrite('combined.png', background)

result: combined image

fast version

I stumbled across this question while trying to add a png overlay to a live video feed. The above solution is way too slow for that. We can make the algorithm significantly faster by using numpy's vector functions.

note: This was my first real foray into numpy so there may be better/faster methods than what I've come up with.

import cv2
import numpy as np

background = cv2.imread('field.jpg')
overlay = cv2.imread('dice.png', cv2.IMREAD_UNCHANGED)  # IMREAD_UNCHANGED => open image with the alpha channel

# separate the alpha channel from the color channels
alpha_channel = overlay[:, :, 3] / 255 # convert from 0-255 to 0.0-1.0
overlay_colors = overlay[:, :, :3]

# To take advantage of the speed of numpy and apply transformations to the entire image with a single operation
# the arrays need to be the same shape. However, the shapes currently looks like this:
#    - overlay_colors shape:(width, height, 3)  3 color values for each pixel, (red, green, blue)
#    - alpha_channel  shape:(width, height, 1)  1 single alpha value for each pixel
# We will construct an alpha_mask that has the same shape as the overlay_colors by duplicate the alpha channel
# for each color so there is a 1:1 alpha channel for each color channel
alpha_mask = np.dstack((alpha_channel, alpha_channel, alpha_channel))

# The background image is larger than the overlay so we'll take a subsection of the background that matches the
# dimensions of the overlay.
# NOTE: For simplicity, the overlay is applied to the top-left corner of the background(0,0). An x and y offset
# could be used to place the overlay at any position on the background.
h, w = overlay.shape[:2]
background_subsection = background[0:h, 0:w]

# combine the background with the overlay image weighted by alpha
composite = background_subsection * (1 - alpha_mask) + overlay_colors * alpha_mask

# overwrite the section of the background image that has been updated
background[0:h, 0:w] = composite

cv2.imwrite('combined.png', background)

How much faster? On my machine the slow method takes ~3 seconds and the optimized method takes ~ 30 ms. So about 100 times faster!

Wrapped up in a function

This function handles foreground and background images of different sizes and also supports negative and positive offsets the move the overlay across the bounds of the background image in any direction.

import cv2
import numpy as np

def add_transparent_image(background, foreground, x_offset=None, y_offset=None):
    bg_h, bg_w, bg_channels = background.shape
    fg_h, fg_w, fg_channels = foreground.shape

    assert bg_channels == 3, f'background image should have exactly 3 channels (RGB). found:{bg_channels}'
    assert fg_channels == 4, f'foreground image should have exactly 4 channels (RGBA). found:{fg_channels}'

    # center by default
    if x_offset is None: x_offset = (bg_w - fg_w) // 2
    if y_offset is None: y_offset = (bg_h - fg_h) // 2

    w = min(fg_w, bg_w, fg_w + x_offset, bg_w - x_offset)
    h = min(fg_h, bg_h, fg_h + y_offset, bg_h - y_offset)

    if w < 1 or h < 1: return

    # clip foreground and background images to the overlapping regions
    bg_x = max(0, x_offset)
    bg_y = max(0, y_offset)
    fg_x = max(0, x_offset * -1)
    fg_y = max(0, y_offset * -1)
    foreground = foreground[fg_y:fg_y + h, fg_x:fg_x + w]
    background_subsection = background[bg_y:bg_y + h, bg_x:bg_x + w]

    # separate alpha and color channels from the foreground image
    foreground_colors = foreground[:, :, :3]
    alpha_channel = foreground[:, :, 3] / 255  # 0-255 => 0.0-1.0

    # construct an alpha_mask that matches the image shape
    alpha_mask = np.dstack((alpha_channel, alpha_channel, alpha_channel))

    # combine the background with the overlay image weighted by alpha
    composite = background_subsection * (1 - alpha_mask) + foreground_colors * alpha_mask

    # overwrite the section of the background image that has been updated
    background[bg_y:bg_y + h, bg_x:bg_x + w] = composite

example usage:

background = cv2.imread('field.jpg')
overlay = cv2.imread('dice.png', cv2.IMREAD_UNCHANGED)  # IMREAD_UNCHANGED => open image with the alpha channel

x_offset = 0
y_offset = 0
print("arrow keys to move the dice. ESC to quit")
while True:
    img = background.copy()
    add_transparent_image(img, overlay, x_offset, y_offset)

    cv2.imshow("", img)
    key = cv2.waitKey()
    if key == 0: y_offset -= 10  # up
    if key == 1: y_offset += 10  # down
    if key == 2: x_offset -= 10  # left
    if key == 3: x_offset += 10  # right
    if key == 27: break  # escape

offset dice

Subjunction answered 1/4, 2022 at 2:12 Comment(3)
Great answer. One small comment - you could leverage numpy's broadcasting for a small bump in speed. alpha_mask = alpha_channel[:,:,np.newaxis]Calvary
Hi @Ben, many thanks for this example! I was trying to convert it to support a transparent background image but I couldn't. Any tips? I like the short version of your code without the offsets.Miscellany
Great! I have only tested your first solution and it works fine. It is quite ironic and totally unfair that it has got only 2/3 of the upvotes of the Mala's solution, which is also the selected answer (!) and which doesn't work!! Well, this is stackoverflow ...Associate
O
26

The following code will use the alpha channels of the overlay image to correctly blend it into the background image, use x and y to set the top-left corner of the overlay image.

import cv2
import numpy as np

def overlay_transparent(background, overlay, x, y):

    background_width = background.shape[1]
    background_height = background.shape[0]

    if x >= background_width or y >= background_height:
        return background

    h, w = overlay.shape[0], overlay.shape[1]

    if x + w > background_width:
        w = background_width - x
        overlay = overlay[:, :w]

    if y + h > background_height:
        h = background_height - y
        overlay = overlay[:h]

    if overlay.shape[2] < 4:
        overlay = np.concatenate(
            [
                overlay,
                np.ones((overlay.shape[0], overlay.shape[1], 1), dtype = overlay.dtype) * 255
            ],
            axis = 2,
        )

    overlay_image = overlay[..., :3]
    mask = overlay[..., 3:] / 255.0

    background[y:y+h, x:x+w] = (1.0 - mask) * background[y:y+h, x:x+w] + mask * overlay_image

    return background

This code will mutate background so create a copy if you wish to preserve the original background image.

Obed answered 6/1, 2019 at 4:57 Comment(8)
A note to follow @Derzu's comment about reading in images with flag IMREAD_UNCHANGED or else an error will be thrown ValueError: operands could not be broadcast together with shapes (790,600,1) (790,600)Anticlerical
This solution is appropriate for images with different shapes, as it positions the overlay anywhere you need, n contrast with @Manivannan Murugavel solution, which only works for images with same sizes.Pippa
If the background of your png image is black instead of transparent try using IMREAD_UNCHANGED while reading the image. https://mcmap.net/q/276381/-problem-about-background-transparent-png-format-opencv-with-pythonDurra
@WillNathan I had to write overlay_image = overlay[..., :overlay.shape[2]] instead overlay_image = overlay[..., :3] to correctly handle images with alpha channel.Custodian
output is float but with values ranging 0 .. 255. imshow will show a blown out picture. fix using .astype(np.uint8) or divide by 255 (value range 0.0 to 1.0)Jarred
How to handle negative x, y values?Zoe
finally a answer that worked for my code! Thanks a ton, i spent way to long with this!Reunionist
Congrats! 👍 This is even a better solution, since one can choose where to place the overlaid image. I think all are good solutions except Mala's, the first one, which got the most upvotes and has been selected as "the answer" and which not only doesn't work but it raises fatal errors! It is weird preferences we are talking about ...Associate
S
15

Been a while since this question appeared, but I believe this is the right simple answer, which could still help somebody.

background = cv2.imread('road.jpg')
overlay = cv2.imread('traffic sign.png')

rows,cols,channels = overlay.shape

overlay=cv2.addWeighted(background[250:250+rows, 0:0+cols],0.5,overlay,0.5,0)

background[250:250+rows, 0:0+cols ] = overlay

This will overlay the image over the background image such as shown here:

Ignore the ROI rectangles

enter image description here

Note that I used a background image of size 400x300 and the overlay image of size 32x32, is shown in the x[0-32] and y[250-282] part of the background image according to the coordinates I set for it, to first calculate the blend and then put the calculated blend in the part of the image where I want to have it.

(overlay is loaded from disk, not from the background image itself,unfortunately the overlay image has its own white background, so you can see that too in the result)

Sesterce answered 18/4, 2018 at 20:4 Comment(1)
this answer doesn't use the alpha channel of the overlayJarred
S
8

You need to open the transparent png image using the flag IMREAD_UNCHANGED

Mat overlay = cv::imread("dice.png", IMREAD_UNCHANGED);

Then split the channels, group the RGB and use the transparent channel as an mask, do like that:

/**
 * @brief Draws a transparent image over a frame Mat.
 * 
 * @param frame the frame where the transparent image will be drawn
 * @param transp the Mat image with transparency, read from a PNG image, with the IMREAD_UNCHANGED flag
 * @param xPos x position of the frame image where the image will start.
 * @param yPos y position of the frame image where the image will start.
 */
void drawTransparency(Mat frame, Mat transp, int xPos, int yPos) {
    Mat mask;
    vector<Mat> layers;

    split(transp, layers); // seperate channels
    Mat rgb[3] = { layers[0],layers[1],layers[2] };
    mask = layers[3]; // png's alpha channel used as mask
    merge(rgb, 3, transp);  // put together the RGB channels, now transp insn't transparent 
    transp.copyTo(frame.rowRange(yPos, yPos + transp.rows).colRange(xPos, xPos + transp.cols), mask);
}

Can be called like that:

drawTransparency(background, overlay, 10, 10);
Saxhorn answered 15/4, 2019 at 18:31 Comment(4)
Appreciate your effort to answer, but the OP had requested for an answer in PythonKnotgrass
great idea to use the transparency layer as a mask for copying ...Nosewheel
your solution is good in my case i just opened the image using the flag in your code "IMREAD_UNCHANGED" this solved my problem . .Cosmography
this solution only uses the alpha channel as a binary mask, not as a factor. it can't handle blending.Jarred
T
2

To overlay png image watermark over normal 3 channel jpeg image

import cv2
import numpy as np
​
def logoOverlay(image,logo,alpha=1.0,x=0, y=0, scale=1.0):
    (h, w) = image.shape[:2]
    image = np.dstack([image, np.ones((h, w), dtype="uint8") * 255])
​
    overlay = cv2.resize(logo, None,fx=scale,fy=scale)
    (wH, wW) = overlay.shape[:2]
    output = image.copy()
    # blend the two images together using transparent overlays
    try:
        if x<0 : x = w+x
        if y<0 : y = h+y
        if x+wW > w: wW = w-x  
        if y+wH > h: wH = h-y
        print(x,y,wW,wH)
        overlay=cv2.addWeighted(output[y:y+wH, x:x+wW],alpha,overlay[:wH,:wW],1.0,0)
        output[y:y+wH, x:x+wW ] = overlay
    except Exception as e:
        print("Error: Logo position is overshooting image!")
        print(e)
​
    output= output[:,:,:3]
    return output

Usage:

background = cv2.imread('image.jpeg')
overlay = cv2.imread('logo.png', cv2.IMREAD_UNCHANGED)
​
print(overlay.shape) # must be (x,y,4)
print(background.shape) # must be (x,y,3)

# downscale logo by half and position on bottom right reference
out = logoOverlay(background,overlay,scale=0.5,y=-100,x=-100) 
​
cv2.imshow("test",out)
cv2.waitKey(0)
Tropous answered 25/11, 2019 at 14:48 Comment(1)
this answer doesn't use the alpha channel of the overlayJarred
A
0
import cv2
import numpy as np

background = cv2.imread('background.jpg')
overlay = cv2.imread('cloudy.png')
overlay = cv2.resize(overlay, (200,200))
# overlay = for_transparent_removal(overlay)
h, w = overlay.shape[:2]
shapes = np.zeros_like(background, np.uint8)
shapes[0:h, 0:w] = overlay
alpha = 0.8
mask = shapes.astype(bool)

# option first
background[mask] = cv2.addWeighted(shapes, alpha, shapes, 1 - alpha, 0)[mask]
cv2.imwrite('combined.png', background)
# option second
background[mask] = cv2.addWeighted(background, alpha, overlay, 1 - alpha, 0)[mask]
# NOTE : above both option will give you image overlays but effect would be changed
cv2.imwrite('combined.1.png', background)

transparent overlay combined.png

combined.1.png

Ambassadoratlarge answered 16/12, 2021 at 11:54 Comment(5)
this answer doesn't use the alpha channel of the overlayJarred
@ChristophRackwitz you are right there is no alpha channel but this is also a different way of doing overlay task.we should know multiple way of doing same task and it's a good thing you know?Ambassadoratlarge
if you use the pictures provided by the question, you'll see.Jarred
@Ambassadoratlarge Very nice idea with the masking to only apply the alpha/blending/mixing in the areas where the shapes are rendered! Your 2 solutions are both confusing though. I have a 3rd method to propose which I think is the most logical: background[mask] = cv2.addWeighted(background, 1 - alpha, overlay, alpha, 0)[mask] ... This behaves the way most people expect: Alpha is how visible you want the OVERLAY shapes to be. So 0.8 means overlay is 80% visible. Etc. :) Feel free to edit this into your answer if you want to. :)Snowblind
@MitchMcMabers that method does not respect the values given in the alpha channel. the question requires a solution that respects the values in the alpha channel, which are individual to every pixel. -- your comment doesn't do that. your comment applies one scalar factor to the entire overlay. that is not addressing the question.Jarred
F
0

Here is another very simple way we can add an transparent overlay image on top of background image:

import numpy as np
import cv2
fsize = 600
img = cv2.imread('football_stadium.png')
overlay_t = cv2.imread('football_3.png',-1) # -1 loads with transparency
overlay_t = cv2.resize(overlay_t, (fsize, fsize))

def overlay_transparent(background_img, img_to_overlay_t, x, y, overlay_size=None):
    """
    @brief      Overlays a transparant PNG onto another image using CV2
    
    @param      background_img    The background image
    @param      img_to_overlay_t  The transparent image to overlay (has alpha channel)
    @param      x                 x location to place the top-left corner of our overlay
    @param      y                 y location to place the top-left corner of our overlay
    @param      overlay_size      The size to scale our overlay to (tuple), no scaling if None
    
    @return     Background image with overlay on top
    """
    
    bg_img = background_img.copy()
    
    if overlay_size is not None:
        img_to_overlay_t = cv2.resize(img_to_overlay_t.copy(), overlay_size)

    # Extract the alpha mask of the RGBA image, convert to RGB 
    b,g,r,a = cv2.split(img_to_overlay_t)
    overlay_color = cv2.merge((b,g,r))
    
    # Apply some simple filtering to remove edge noise
    mask = cv2.medianBlur(a,5)

    h, w, _ = overlay_color.shape
    roi = bg_img[y:y+h, x:x+w]

    # Black-out the area behind the logo in our original ROI
    img1_bg = cv2.bitwise_and(roi.copy(),roi.copy(),mask = cv2.bitwise_not(mask))
    
    # Mask out the logo from the logo image.
    img2_fg = cv2.bitwise_and(overlay_color,overlay_color,mask = mask)

    # Update the original image with our new ROI
    bg_img[y:y+h, x:x+w] = cv2.add(img1_bg, img2_fg)

    return bg_img

game_window = "game_window"
cv2.namedWindow(game_window, cv2.WINDOW_NORMAL)
cv2.resizeWindow(game_window, 800, 600)
start_x = 2700
start_y = 3600
cv2.imshow(game_window, overlay_transparent(img, overlay_t, start_x, start_y, (fsize,fsize)))
cv2.waitKey(0)
Famous answered 10/4, 2023 at 12:34 Comment(0)
A
-2

**Use this function to place your overlay on any background image. if want to resize overlay use this overlay = cv2.resize(overlay, (200,200)) and then pass resized overlay into the function. **

import cv2
import numpy as np


def image_overlay_second_method(img1, img2, location, min_thresh=0, is_transparent=False):
    h, w = img1.shape[:2]
    h1, w1 = img2.shape[:2]
    x, y = location
    roi = img1[y:y + h1, x:x + w1]

    gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
    _, mask = cv2.threshold(gray, min_thresh, 255, cv2.THRESH_BINARY)
    mask_inv = cv2.bitwise_not(mask)

    img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)
    img_fg = cv2.bitwise_and(img2, img2, mask=mask)
    dst = cv2.add(img_bg, img_fg)
    if is_transparent:
        dst = cv2.addWeighted(img1[y:y + h1, x:x + w1], 0.1, dst, 0.9, None)
    img1[y:y + h1, x:x + w1] = dst
    return img1

if __name__ == '__main__':
    background = cv2.imread('background.jpg')
    overlay = cv2.imread('overlay.png')
    output = image_overlay_third_method(background, overlay, location=(800,50), min_thresh=0, is_transparent=True)
    cv2.imwrite('output.png', output)

background.jpg output

output.png enter image description here

Ambassadoratlarge answered 16/12, 2021 at 12:10 Comment(3)
this answer doesn't use the alpha channel of the overlayJarred
@ChristophRackwitz you are right there is no alpha channel but this is also a different way of doing overlay task.we should know multiple way of doing same task and it's a good thing you know?Ambassadoratlarge
if you use the pictures provided by the question, you'll see.Jarred

© 2022 - 2024 — McMap. All rights reserved.