简体   繁体   中英

Set the background color for a PNG with transparency

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.]

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()

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:

来自维基百科的原始标志 在此处输入图像描述

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).

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM