Mastering Transparent Images: Adding a Background Layer

Author:Murphy  |  View: 30106  |  Time: 2025-03-22 19:04:58
Adding a Colored Background to an Image with an Alpha Channel

Recently, I needed to add a white background to a couple of images with a transparent background. Naturally, I used Python with Opencv to automate the process, I wasn't going to open an image editor for each of these images.

Adding a background color sounds easy enough, right?

Well, on my first attempt at implementing this, I didn't quite succeed and had to iterate on the solution, so I thought I would share the process with you.

If you want to follow along, make sure to install the opencv-Python and numpy package in your local Python environment. You can use the following transparent image of an equation for experimentation, you can download it from here .

PNG Image with Alpha Channel

Load Image with Alpha Channel

In a first step, we need to load the transparent image including the alpha channel. If we load the image normally with cv2.imread without the required parameter, it will just ignore the alpha channel and the image will be completely black in this case.

img = cv2.imread("equation.png")

cv2.imshow("Image", img)

cv2.waitKey(0)
cv2.destroyAllWindows()
Reading the PNG image without the Alpha Channel

If you look at the shape of the image, you will notice that there are only 3 color channels for the blue, green and red values, but no alpha channel.

# Shape of image in order (H, W, C) with H=height, W=width, C=color
print(img.shape)
> (69, 244, 3)

There's a flag parameter in the imread function, which defaults to _cv2.IMREAD_COLOR. This will always convert the image to a 3 channel BGR image. To keep the alpha channel, we need to specify the flag cv2.IMREAD_UNCHANGED_.

img = cv2.imread("equation.png", flags=cv2.IMREAD_UNCHANGED)

# Shape of image in order (H, W, C) with H=height, W=width, C=color
print(img.shape)
> (69, 244, 4)

As you can see now, the alpha channel is also included and the color channel order is BGRA, so for each pixel we get 4 values where the last one is the alpha value.

If you try to show the image with imshow, you will see that it is still black. The function simply ignores the alpha channel, but we know it is there. **** Now we can start working on replacing the alpha channel with a colored background.

Binary Background

In a first attempt, we could try to replace the transparent pixels of the image with the background color, e.g. white. We can for example set all the pixels with more than 50% transparency to white. Since the image array is represented with 8-bit unsigned integers, the value of the alpha channel reaches from 0 to 255. Hence we can replace all pixels that have an alpha value of less than or equal 128 (50% of 256) with white (all 3 color channels set to 255).

# Values for background
bg_threshold = 128 # 50% transparency
bg_color = (255, 255, 255) # White

# Threshold alpha channel
bg_indices = img[:, :, 3] <= bg_threshold

# Replace image at indices with background color
img[bg_indices, :3] = bg_color

And here you can see the result:

You can also try it with different thresholds, in the extreme case with a threshold of 0, so only 100% transparent pixels are considered background and hence colored white.

Background Blending

As you might have already noticed, both of these images don't look quite right. You can probably find a better threshold that makes the image look decent, but the underlying problem is that we are setting a binary threshold, whereas the opacity / alpha channel is a continuous value. The background color should actually be blended with the original color, so let's see how we can achieve this.

To blend the original color with a background color based on the alpha value, we want the following behavior:

  • At 100% opacity, we should apply 100% of the original color
  • At 60% opacity, we want 60% of the original color and 40% of the background color.
  • At 0% opacity we want only the background color.

In other words, the color of the final image for each pixel should be as follows, where alpha is a value between 0 and 1:

# alpha=0 -> 0% opacity, fully transparent
# alpha=1 -> 100% opacity, fully opaque
color_final = alpha * original + (1 - alpha) * background

To achieve this, we first have to extract the alpha channel and normalize it. Make sure to convert the data type to float before normalization, with the original uint8 data type we would only get 0s and 1s.

NOTE: We divide by 255 instead of 256, since the maximum value of an 8-bit integer can be 255, and we want that to be exactly 1.

alpha = img[:, :, 3].astype(float)
alpha = alpha / 255.0

Next we should prepare the original 3 color channel image by indexing the first 3 channels in the BGRA image. We also need to convert these values to floats, otherwise we won't be able to multiply and add them together later with the alpha value.

# BGR for blue, green, red
img_bgr_original = img[:, :, :3].astype(float)

We can also prepare the background image, consisting of our background color at each pixel. Using the _full_like function of numpy, we can repeat a value, in our case the background color, in a way that results in the same shape as the input array, our image. Again, using the float_ data type.

img_bgr_background = np.full_like(a=img_bgr_original, fill_value=bg_color, dtype=float)

Now we are almost ready to multiply with our formula from above. In fact, let's try it and see what happens, so you can also understand why we need to do an extra step.

img_blended = alpha * img_bgr_original + (1 - alpha) * img_bgr_background

If we run this, we get the following error:

    img_blended = alpha * img_bgr_original + (1 - alpha) * img_bgr_background
                  ~~~~~~^~~~~~~~~~~~~~~~~~
ValueError: operands could not be broadcast together with shapes (69,244) (69,244,3) 

The problem is, that our alpha image has a shape (69, 244) of dimension 2, there's no color channel dimension. Whereas the images have a color channel dimension (69, 244, 3), the dimensionality is 3. To fix this, we want to make sure both arrays have 3 dimensions, then we will be able to multiply them together. To do this, we can expand the dimension of our alpha array using the _np.expand_dims_ function.

print(f"{img.shape=}") # img.shape=(69, 244, 4)
print(f"{img_bgr_original.shape=}") # img_bgr_original.shape=(69, 244, 3)
print(f"{img_bgr_background.shape=}") # img_bgr_background.shape=(69, 244, 3)
print(f"{alpha.shape=}") # alpha.shape=(69, 244)

alpha = np.expand_dims(alpha, axis=-1)

print(f"{alpha.shape=}") # alpha.shape=(69, 244, 1) <- see the additional dimension here!

This concept is extremely important but not quite easy to understand. I encourage you to spend some time to understand and experiment with the array shapes.

Now we're finally able to calculate the blended image with the code from above. One final step we need before we can show the image, is convert it back to an 8-bit unsigned integer array.

img_blended = alpha * img_bgr_original + (1 - alpha) * img_bgr_background
img_blended = img_blended.astype(np.uint8)

cv2.imshow("Blended Background", img_blended)

Now we can visualize the blended image and see the clean result:

We can also change the background color to an orange color for example, and add some padding around the image with the background color.

bg_color = (200, 220, 255) # Orange
bg_padding = 20

...

img_blended = cv2.copyMakeBorder(
    img_blended, bg_padding, bg_padding, bg_padding, bg_padding, cv2.BORDER_CONSTANT, value=bg_color
)

...

Conclusion

In this project, you learned how to add a background layer to an image with transparency. We first explored a binary method that uses a threshold to set the background color in the image. However, this approach did not produce visually satisfactory results. In a second attempt, we used an approach that blends the background color based on the alpha value. This resulted in clean images with a naturally looking background. We looked into some details regarding image dimensionalities and worked through a common error that can happen when working with numpy arrays.

The full source code is available from GitHub below. I hope you learned something!

image-processing/transparent_background.py at main · trflorian/image-processing


All visualizations in this post were created by the author.

Tags: Computer Vision Data Science Opencv Programming Python

Comment