-1

I've been using opencv to paste a png image over a background. My target is to simulate a sort of plastic bag underwater. In that way, I would like to apply Alpha in order to have the png image semi-transparent over the background, but when I do, the black mask of the png becomes transparent too.

y1, y2 = yoff, yoff + png.shape[0]
x1, x2 = xoff, xoff + png.shape[1]

alpha_png = png[:, :, 3] / 255.0
alpha_bg = 0.5 - alpha_png

bg_copy = bg.copy()

for c in range(0, 3):
    bg_copy[y1:y2, x1:x2, c] = (alpha_png * png[:, :, c] + alpha_bg * bg_copy[y1:y2, x1:x2, c])
cv2.imwrite("result.png", bg_copy)

I found this code online, and I changed the alpha_bg in order to make the image more or less transparent.

And the result is png image on background

I need to remove the black semi-transparent background, but the rest of the png should remain semi-transparent, plus the bag should get the background green color or underwater color (But I'll work later on this).

Any suggestion is really appreciated, also considering to use PIL instead of opencv.

Edit:

The used images are: Foreground foreground

And Background: background

ShottyNo
  • 63
  • 1
  • 7
  • 2
    this alpha, does it come from the green image, or from the trash bag image? [mre] please, including the input images. – Christoph Rackwitz May 29 '23 at 16:26
  • duplicate of https://stackoverflow.com/questions/40895785/using-opencv-to-overlay-transparent-image-onto-another-image – Christoph Rackwitz May 29 '23 at 18:35
  • @ChristophRackwitz I tried this post, in particular the reply from Ben. I used the `def add_transparent_image()` function, but it not work as expected, because when I give alpha < 255, foreground image's colors changes in a not normal way. – ShottyNo May 29 '23 at 19:35
  • 1
    this isn't a normal "overlay" situation. you'd want the water to be the *foreground* because it's supposed to shade the trash bag. the solution by fmw42 is equivalent to what I would have posted. for this specific case, you could simply have multiplied the bag's alpha channel by the complement of the water's alpha. that would simplify the calculations greatly. – Christoph Rackwitz May 29 '23 at 21:16

4 Answers4

1

Here is one way to do that in Python/OpenCV.

  • Read the background (water) image and get its shape
  • Read the foreground (bag) image and get its shape
  • Separate the bag's BGR channels and its alpha channel
  • Extend the bag BGR image with transparent black as a mask - insert the bag at the center of the water inside black the size of the water image
  • Similarly extend the bag's alpha channel in black the size of the water image at the center (or where ever you want)
  • Define the blend factor
  • Convert the mask to float, divide by 255 and multiply by the blend amount
  • Convert the bag's BGR to float and use the mask to blend with the water
  • Clip the result and convert back to uint8
  • Save the result

Water:

enter image description here

Bag:

enter image description here

import cv2
import numpy as np

# read the background image
water = cv2.imread('background.jpg')
h, w = water.shape[:2]
cx = w//2
cy = h//2

# read the foreground image
bag = cv2.imread('bag.png', cv2.IMREAD_UNCHANGED)
hh, ww = bag.shape[:2]

# separate bgr and alpha in bag image
bag_bgr = bag[:,:,0:3]
bag_alpha = bag[:,:,3]

# extend bag to size of water with added area black and transparent
# then insert bag at center of water
bag_ext = np.zeros((h,w,3), dtype=np.uint8)
bag_ext[cy:cy+hh, cx:cx+ww] = bag_bgr

mask = np.zeros((h,w), dtype=np.uint8)
mask[cy:cy+hh, cx:cx+ww] = bag_alpha
mask = cv2.merge([mask,mask,mask])

# blend bag_bgr with water using bag_alpha
blend = 0.5
mask = blend*mask.astype(np.float64)/255
result = bag_ext.astype(np.float64) * mask + water.astype(np.float64) * (1-mask)
result = result.clip(0,255).astype(np.uint8)

# save results
cv2.imwrite('water_bag.jpg', result)

# show results
cv2.imshow('result', result)
cv2.waitKey(0)

Result for blend=0.5:

enter image description here

fmw42
  • 46,825
  • 10
  • 62
  • 80
1

An equivalent alternative to fmw42's answer:

composite = water.copy()
roi = composite[cy:cy+hh, cx:cx+ww]

water_alpha = 0.5 # how much it affects underlaid objects

# bake the _remainder/complement_ of the overlay's (water) alpha into the underlay's (bag) alpha
# also /255 so the value range is 0.0 to 1.0
# float32 is a cheaper data type
alpha = bag_alpha * np.float32((1 - water_alpha) / 255)

# add a dimension for broadcasting along color channels
alpha = alpha[..., None]

# convex combination, no need to clip
roi[:] = bag_bgr * alpha + roi * (1 - alpha)

Result is the same.

Christoph Rackwitz
  • 11,317
  • 4
  • 27
  • 36
  • Great suggestion to crop rather than pad. I have filled out this approach in a new answer. Feel free to modify or comment. – fmw42 May 29 '23 at 22:04
  • `@Christoph Rackwitz` I have added an enhancement request to OpenCV to do the above. Feel free to add to it or correct it. – fmw42 May 29 '23 at 22:23
  • https://github.com/opencv/opencv/issues/20780 – Christoph Rackwitz May 29 '23 at 23:10
  • that "weight" is just a flat alpha channel for one of the layers. the "mask" is an actual alpha channel. the word "mask" should never be used in this context because masks imply boolean element values. – Christoph Rackwitz May 29 '23 at 23:12
  • `@Christoph Rackwitz` Great! More requests might get more attention. Thanks for your post an all the work you put into it. – fmw42 May 29 '23 at 23:17
  • `@Christoph Rackwitz` Pardon me about terminology. I use terminology rather loosely and it may not fit with that used with OpenCV. – fmw42 May 29 '23 at 23:19
1

Filling out the the details from the answer according Christoph Rackwitz, we crop the water image at the desired location to the size of the bag and blend with the bag image (bgr) using the mask from the alpha channel. Then we insert the resulting composite into a copy of the water image at the desired location.

import cv2
import numpy as np

# read the background image
water = cv2.imread('background.jpg')
h, w = water.shape[:2]
cx = w//2
cy = h//2

# read the foreground image
bag = cv2.imread('bag.png', cv2.IMREAD_UNCHANGED)
hh, ww = bag.shape[:2]

# separate bgr and alpha in bag image
bag_bgr = bag[:,:,0:3]
bag_alpha = bag[:,:,3]

# crop the water image at center to size of bag
water2 = water.copy()
water_crop = water2[cy:cy+hh, cx:cx+ww]

blend = 0.5
mask = blend*bag_alpha.astype(np.float32)/255
mask = mask[..., None]
composite = bag_bgr.astype(np.float32) * mask + water_crop.astype(np.float32) * (1-mask)
composite = composite.astype(np.uint8)

# insert composite into water
result = water.copy()
result[cy:cy+hh, cx:cx+ww] = composite

# save results
cv2.imwrite('water_bag2.jpg', result)

# show results
cv2.imshow('result', result)
cv2.waitKey(0)

Result:

enter image description here

fmw42
  • 46,825
  • 10
  • 62
  • 80
0

The correct term to search for is "alpha blending", but here's a quick example adding the python logo to your image.

source images: from here and from your post

foreground image background image

from PIL import Image
import numpy as np

with Image.open(r"python-logo-only.png") as img: #you'll probably have to adjust file names to run this yourself
    image_with_alpha = np.array(img)
    
with Image.open(r"UNhSa.jpg") as img:
    background = np.array(img) #image without alpha
    
#we need images of the same size so we can add them together easily
foreground = np.zeros_like(background)

#copy image color data to forground layer
w, h, _ = image_with_alpha.shape
foreground[:w, :h, :] = image_with_alpha[:,:,:3] #copy color data not alpha layer

#make a transparency mask
transparency = np.zeros(background.shape[:2], dtype = float)
#copy transparency layer data from foreground image
transparency[:w, :h] = image_with_alpha[:,:,3] / 255 #convert 0-255 range to 0-1

#multiply transparency by color data to get forground contribution to final image
foreground_multiplied = foreground * transparency[:,:,None] #add None (np.newaxis) to get correct ndim
#multiply inverse transparency by background to get background contribution to final image
background_multiplied = background * (1-transparency[:,:,None])
#add the two images
final_image = foreground_multiplied + background_multiplied

img = Image.fromarray(final_image.astype(np.uint8))
img.show()

final image:

final image

PIL also has built-in methods to do much of this in PIL.Image.alpha_composite and PIL.Image.composite, but you will still need to adjust the images to the same dimensions. Image.paste is a nearly complete solution in just one line.

Aaron
  • 10,133
  • 1
  • 24
  • 40
  • PIL has `Image.paste()` so if you chose to go with PIL, all that array stuff is superfluous. apart from that, the question (at least as you interpret it) is a duplicate of https://stackoverflow.com/questions/40895785/using-opencv-to-overlay-transparent-image-onto-another-image – Christoph Rackwitz May 29 '23 at 18:33
  • @ChristophRackwitz I missed `paste` when skimming through `Image`. I mostly wanted the example to be an explanation of what all needs to be done under the hood to promote understanding of the problem. `Image.paste` does indeed do pretty much all of it though. – Aaron May 29 '23 at 20:32