Set the background color for a PNG with transparency
Asked Answered
I

3

8

I am loading PNG images that have a transparency plane. When converted to grayscale, the transparent areas in the image appear as black, which seems to be the default background. I need them to be white instead. What can I do ?

[This is not the usual question on how to preserve transparency.]

Icj answered 10/8, 2017 at 10:51 Comment(10)
Is the transparency plane uniform ? (i.e. how many values can the alpha component take ? Two ?) First idea: draw the image over a white background image before converting to grayscale. Second idea: convert to grayscale first and then iterate over each pixel and turn to white in new image all pixels that have in original image their alpha component below a given threshold.Merriemerrielle
@GabrielDevillers: actually I don't know. I am reading the PNG and get the grayscale image with the black pixels. I don't see the alpha plane.Icj
@YvesDaoust How do you load the image? I guess as grayscale, based on your description. You'll need to load it with IMREAD_UNCHANGED, so that you get the full 4 channel image, and do some post-processing on that.Cataplasm
@DanMašek: yep, as a last resort I'll do this. But I am trying again: is there a way to specify a background color other than black ?Icj
@YvesDaoust I believe what you ask makes no sense: if you drop the alpha channel then you get a color that is defined by the 3 remaining RGB channels. In your case the color of the transparent (probably fully transparent) pixels is black, hence the result. In other words: any transparent pixel has a RGB color that is not visible if the pixel is fully transparent, in your case the color of your transparent pixels is black.Merriemerrielle
@YvesDaoust As far as I can tell, there's no support for that in the PngDecoder. I guess it would need a call to png_set_background somewhere.Cataplasm
@GabrielDevillers: I would tend to think that the transparent parts of the image are white, not black: whenI open the image in Paint or convert to a BMP in Photoshop, the background is white.Icj
@DanMašek: by looking at some OpenCV source code, see a macro PNG_READ_BACKGROUND_SUPPORTED used in relation to png_set_background. So I am tempted to think that the particular version I am using does not support background color retrieval.Icj
@YvesDaoust You mean this (the libpng source code included with OpenCV)? The flag seems to be defined in pnglibconf.h, and has been for a while (looking through historical tags). The OpenCV image codec wrapper just doesn't provide any means to use this functionality -- kind of understandable since imread only takes a single flag, which would make it hard to pass arbitrary 1 or 3 channel colour.Cataplasm
@DanMašek: yep, this is the flag I am referring to. I was dreaming of a backdoor...Icj
T
1

The most effective way (memory and CPU) would be to let libPNG do it, using png_set_background:

If you don't need, or can't handle, the alpha channel you can call png_set_background() to remove it by compositing against a fixed color. Don't call png_set_strip_alpha() to do this - it will leave spurious pixel values in transparent parts of this image.

png_set_background(png_ptr, &background_color,
   PNG_BACKGROUND_GAMMA_SCREEN, 0, 1);

The background_color is an RGB or grayscale value according to the data format libpng will produce for you.

Unfortunately, the OpenCV wrapper around libPNG doesn't use this, so you'd have to patch in some rudimentary support yourself (hindered by the limited ability to pass additional options to imread).

Other possible approach would be to just write your own simple image loader using libPNG for this specific purpose.

If you can afford some waste, load it as BGRA, and do some post-processing. However I'd go a step further than the code referred to by Gabriel and incorporate the color conversion in it.

void remove_transparency(cv::Mat const& source
    , cv::Mat& destination
    , uint8_t background_color)
{
    CV_Assert(source.type() == CV_8UC4);

    destination.create(source.rows, source.cols, CV_8UC1);

    auto it_src(source.begin<cv::Vec4b>()), it_src_end(source.end<cv::Vec4b>());
    auto it_dest(destination.begin<uint8_t>());

    std::transform(it_src, it_src_end, it_dest
        , [background_color](cv::Vec4b const& v) -> uchar
        {
            // Conversion constants taken from cvtColor docs...
            float gray(v[0] * 0.114f + v[1] * 0.587f + v[2] * 0.299f);
            float alpha(v[3] / 255.0f);
            return cv::saturate_cast<uchar>(gray * alpha + background_color * (1 - alpha));
        }
        );
}

Of course, this is still single threaded, so let's leverage cv::parallel_for_ to improve it a bit further.

class ParallelRemoveTransparency
    : public cv::ParallelLoopBody
{
public:
    ParallelRemoveTransparency(cv::Mat const& source
        , cv::Mat& destination
        , uint8_t background_color)
        : source_(source)
        , destination_(destination)
        , background_color_(background_color)
    {
        CV_Assert(source.size == destination.size);
    }

    virtual void operator()(const cv::Range& range) const
    {
        cv::Mat4b roi_src(source_.rowRange(range));
        cv::Mat1b roi_dest(destination_.rowRange(range));

        std::transform(roi_src.begin(), roi_src.end(), roi_dest.begin()
            , [this](cv::Vec4b const& v) -> uint8_t {
                float gray(v[0] * 0.114f + v[1] * 0.587f + v[2] * 0.299f);
                float alpha(v[3] / 255.0f);
                return cv::saturate_cast<uint8_t>(gray * alpha + background_color_ * (1 - alpha));
            }
            );
    }

private:
    cv::Mat const& source_;
    cv::Mat& destination_;
    uint8_t background_color_;
};

void remove_transparency(cv::Mat const& source
    , cv::Mat& destination
    , uint8_t background_color)
{
    CV_Assert(source.type() == CV_8UC4);

    destination.create(source.rows, source.cols, CV_8UC1);

    ParallelRemoveTransparency parallel_impl(source, destination, background_color);
    cv::parallel_for_(cv::Range(0, source.rows), parallel_impl);
}

It turns out you need this in Python. Here's a quick little draft of an alternative:

import numpy as np
import cv2

def remove_transparency(source, background_color):
    source_img = cv2.cvtColor(source[:,:,:3], cv2.COLOR_BGR2GRAY)
    source_mask = source[:,:,3]  * (1 / 255.0)

    background_mask = 1.0 - source_mask

    bg_part = (background_color * (1 / 255.0)) * (background_mask)
    source_part = (source_img * (1 / 255.0)) * (source_mask)

    return np.uint8(cv2.addWeighted(bg_part, 255.0, source_part, 255.0, 0.0))


img = cv2.imread('smile.png', -1)
result = remove_transparency(img, 255)

cv2.imshow('', result)
cv2.waitKey()
Terrorist answered 10/8, 2017 at 16:9 Comment(3)
Thanks for all the valuable info. I am working under Python, so implementing this is somewhat out of the question and this is why I was looking for a more OpenCVish solution.Icj
Ahh, I see. There wasn't a language tag in the question, so based on your profile I assumed you'd want C++ :)Cataplasm
@YvesDaoust Added a Python solution. Not sure you can do much better easily.Cataplasm
M
0

If you read a PNG with imread without passing IMREAD_UNCHANGED then you will have a 3 channel BGR image. If there was a fourth alpha channel (0 = fully transparent, 255 = fully visible) then it gets cropped as the documentation put it.

You are getting black pixels where you had transparent pixels simply because the BGR part of the pixel gives a black color. (Vec3b(0, 0, 0)).

If you are not convinced, try to open as BGR (imread wihout IMREAD_UNCHANGED parameter) and display (imshow then waitkey both images below:

original logo from wikipedia enter image description here

While they look similar on this page or in Gimp, The first should have a black background whereas the second one should have a red background.

First solution: use Michael Jepson's overlayImage function

#include <opencv2/highgui/highgui.hpp> 
#include <opencv2/imgcodecs.hpp>
int main(int argc, char** argv ) {
   cv::Mat img_4_channels;
   img_4_channels = cv::imread(argv[1], cv::IMREAD_UNCHANGED); // gives 8UC4
   // img_4_channels = cv::imread(argv[1]); // inappropriate: gives 8UC3

   cv::Mat background = cv::Mat(img_4_channels.size(), CV_8UC3, cv::Vec3b(255, 255, 255)); // white background
   overlayImage(background, img_4_channels, img_3_channels, cv::Point2i(0, 0));

   cv::imshow("3 channels", img_3_channels);
}

Second solution: this answer by Rosa Gronchi

This solution is more lightweight (no coordinate of foreground, no need to allocate a background image).

Merriemerrielle answered 10/8, 2017 at 13:58 Comment(0)
A
0

You can use the following code

def read_transparent_png(filename, hexcode):
    image_4channel = cv2.imread(filename, cv2.IMREAD_UNCHANGED)
    alpha_channel = image_4channel[:, :, 3]
    rgb_channels = image_4channel[:, :, :3]
    white_background_image = np.zeros((image_4channel.shape[0], image_4channel.shape[1],3), dtype=np.uint8)
    rgb = tuple(int(hexcode[i:i+2], 16) for i in (0, 2, 4))
    RED, GREEN, BLUE = rgb[0], rgb[1], rgb[2]
    white_background_image[::] = (BLUE, GREEN, RED)
    alpha_factor = alpha_channel[:, :, np.newaxis].astype(np.float32) / 255.0
    alpha_factor = np.concatenate(
        (alpha_factor, alpha_factor, alpha_factor), axis=2)
    base = rgb_channels.astype(np.float32) * alpha_factor
    white = white_background_image.astype(np.float32) * (1 - alpha_factor)
    final_image = base + white
    return final_image.astype(np.uint8)

here hexcode is the hexadecimal code of the colour that you want to set as background for transparent PNG.

Albanese answered 8/7, 2022 at 10:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.