7
votes

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

2
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.Gabriel Devillers
@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.Yves Daoust
@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.Dan Mašek
@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 ?Yves Daoust
@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.Gabriel Devillers

2 Answers

1
votes

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

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