1

My backend code stores images with alpha to maintain compatibility with as many formats as possible, but when I try to resize and convert to JPG (no alpha), I get all black where the alpha channel wasn't set.

// Creation
BitmapImage bmp = new BitmapImage();
bmp.BeginInit();
bmp.DecodePixelWidth = decodeWidth;
bmp.DecodePixelHeight = decodeHeight;
bmp.UriSource = new Uri(Filename);
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit();
bmp.Freeze();

// Scaling
var scalar = new ScaleTransform(scale, scale);
var bmp = new TransformedBitmap(img, scalar);
bmp.Freeze();

// Testing
var formatted = new FormatConvertedBitmap(bmp, PixelFormats.Bgr32, null, 0);
PngBitmapEncoder png = new PngBitmapEncoder();
MemoryStream ms = new MemoryStream();
png.Frames.Add(BitmapFrame.Create(formatted);
png.Save(ms);
File.WriteAllBytes(dest, ms);

Clearly all that code isn't in the same function, but the gist is there. When the FormatConvertedBitmap is saved AFTER SCALING, it's black where there's alpha specified in the original bitmap. Before scaling, it works as expected. No blackness.

Comparison (removed due to link restrictions)

You can see from the image above that most of the original has an alpha channel, which gets converted to black but ONLY after the transformation.

I found this Saving Windows.Media.Drawing with BmpBitmapEncoder black image - how to remove alpha? and my problem is similar, except that this isn't just blank backgrounds. There's detail "under" this blackness.

TLDR transforming/scaling seems to break the ability to change pixel formats in WPF.
What I want: Non black image. The RGB underneath the alpha definitely isn't black.
What I got: Black image where alpha channel had some influence on the outcome.
Cause: As far as I can tell, it's due to re-sizing.
Am I doing something wrong, or is this some weird bug?

Original dds
https://dl.dropboxusercontent.com/u/37301843/MASSEFFECT3.EXE_0xCFC054A8%20No%20MIPS.dds

TEST PROJECT https://dl.dropboxusercontent.com/u/37301843/StackOverflowExample.7z
This project fully demonstrates my situation.
It requires Windows 8.1+ (for dds codecs) and .NET 4.6.

Thought/testing process
I figure I'll add some context to how I got to this stage.
1. Wrote a dds friendly image converter (original purpose of this tool actually) 2. Discovered that dds' with alpha channels turned out black when converted to jpg using the JpegBitmapEncoder class.
3. Figured by the patterning that it was an alpha problem.
4. Debugging via saving image at various points showed the image was fine until it was resized.
5. Before resizing, saving as jpg works. No blackness.
6. After resizing, jpg is black in all areas where alpha = 0.
7. Figured the encoder was premultiplying alpha such that the resulting pixels were black.
8. Couldn't work out why scaling would do such a thing, but tried stripping out the alpha by converting to Bgr32.
9. Still black when converting after scaling.

Community
  • 1
  • 1
Kael
  • 11
  • 5
  • Can you upload an image that demonstrates this behavior? Is the problem that areas with partial transparency are becoming completely black? – dbc Dec 26 '15 at 08:08
  • I created the following very simple test case: http://i.stack.imgur.com/ARfiC.png. If I load it with your `// Creation` code then save it with your `// Testing` code, the transparent regions turn black - with or without scaling. Can you upload an image that only turns black when scaled? – dbc Dec 26 '15 at 08:30
  • Is your issue that you have a PNG that has pixels that are completely transparent, but also have some "hidden" color data? Along the lines of [Resize PNG image without losing color data from fully transparent pixels](https://stackoverflow.com/questions/28467645)? – dbc Dec 26 '15 at 08:56
  • I tried png's like that too, and they do go black. Not sure why...but if you do it with the dds (linked in OP now) you get back the normal rgb. Maybe it's black cos the background is default black or something? The problem is that any transparency becomes completely black when resized and saved as a JPG. I know JPG doesn't support argb, so after it went black saving normally, I tried stripping the alpha out by using the FormatConvertedBitmap, but no cigar. EDIT: That thread you linked = my problem, but I don't want another image library. That's what this project is avoiding. – Kael Dec 26 '15 at 08:57
  • I can't load a DDS file, and can't reproduce the exact behavior you are seeing with any image I have made. If the issue is that rescaling discards hidden colors for fully transparent pixels, but format conversion doesn't, why not do the format conversion first? – dbc Dec 26 '15 at 09:02
  • That does work. At the time, it was going to be a pain to re-write whole sections of the program in order for it to know the destination format before resizing, but now that seems to be the easiest, perhaps the only, way to move forward. Much thanks dbc. Though, I'm hesitant to make this the answer, since it doesn't explain why scaling breaks things. – Kael Dec 26 '15 at 09:17
  • Kael - the reason we are struggling is that your question doesn't have a [minimal but complete example](http://stackoverflow.com/help/mcve) that reproduces the problem. E.g.: a link to a PNG + code that loads, formats and saves that PNG with no blackening + code that loads, scales, formats and saves that PNG with blackening. – dbc Dec 26 '15 at 09:20
  • Roger that. I'll whip up a small project to demonstrate. – Kael Dec 26 '15 at 09:34
  • Done. Doesn't have the fancy stuff like converting to bgr32, but that doesn't work anyway. – Kael Dec 26 '15 at 09:50

2 Answers2

0

I don't like this as an answer as it's more of a workaround than fixing why scaling doesn't preserve alpha.

BUT here's my workaround. Performs surprisingly well.
1) Pull out alpha channel.
2) Build new bitmap using the alpha values as all rgb channels i.e. rgb = alpha so that the resulting image is grey scale.
3) Change original image's colour space from ARGB to RGB. Since alpha is removed, this doesn't break anything.
4) Scale both images (original RGB and original alpha).
5) Change scaled RGB colourspace back to ARGB (Again, this doesn't really change anything).
6) Merge the scaled alpha into scaled RGB (now ARGB) bitmap.

WriteableBitmap bmp = mipMap.BaseImage;
int origWidth = bmp.PixelWidth;
int origHeight = bmp.PixelHeight;
int origStride = origWidth * 4;
int newWidth = (int)(origWidth * scale);
int newHeight = (int)(origHeight * scale);
int newStride = newWidth * 4;



// Pull out alpha since scaling with alpha doesn't work properly for some reason
WriteableBitmap alpha = new WriteableBitmap(origWidth, origHeight, 96, 96, PixelFormats.Bgr32, null);
unsafe
{
    int index = 3;
    byte* alphaPtr = (byte*)alpha.BackBuffer.ToPointer();
    byte* mainPtr = (byte*)bmp.BackBuffer.ToPointer();
    for(int i = 0; i < origWidth * origHeight * 3; i += 4)
    {
        // Set all pixels in alpha to value of alpha from original image - otherwise scaling will interpolate colours
        alphaPtr[i] = mainPtr[index];
        alphaPtr[i+1] = mainPtr[index];
        alphaPtr[i+2] = mainPtr[index];
        alphaPtr[i+3] = mainPtr[index];
        index += 4;
    }
}

FormatConvertedBitmap main = new FormatConvertedBitmap(bmp, PixelFormats.Bgr32, null, 0);

// Scale RGB and alpha
ScaleTransform scaletransform = new ScaleTransform(scale, scale);
TransformedBitmap scaledMain = new TransformedBitmap(main, scaletransform);
TransformedBitmap scaledAlpha = new TransformedBitmap(alpha, scaletransform);

// Put alpha back in
FormatConvertedBitmap newConv = new FormatConvertedBitmap(scaledMain, PixelFormats.Bgra32, null, 0);
WriteableBitmap resized = new WriteableBitmap(newConv);
WriteableBitmap newAlpha = new WriteableBitmap(scaledAlpha);
unsafe
{
    byte* resizedPtr = (byte*)resized.BackBuffer.ToPointer();
    byte* alphaPtr = (byte*)newAlpha.BackBuffer.ToPointer();
    for (int i = 3; i < newStride; i += 4)
        resizedPtr[i] = alphaPtr[i];
}

Seems to perform ok, but more importantly it does what I want how I need it.

Kael
  • 11
  • 5
0

I noticed the same issue and modify a little bit your version. Alpha is put in a gray8 bitmap before scaling.

    private static BitmapSource GetAphaAsGrayBitmap(BitmapSource rgba)
    {
        WriteableBitmap bmp = new WriteableBitmap(rgba);
        WriteableBitmap alpha = new WriteableBitmap(rgba.PixelWidth, rgba.PixelHeight, 96, 96, PixelFormats.Gray8, null);

        unsafe
        {
            byte* alphaPtr = (byte*)alpha.BackBuffer.ToPointer();
            byte* mainPtr = (byte*)bmp.BackBuffer.ToPointer();
            for (int i = 0; i < bmp.PixelWidth * bmp.PixelHeight; i++)
                alphaPtr[i] = mainPtr[i * 4 + 3];
        }

        return alpha;
    }

    private static BitmapSource MergeAlphaAndRGB(BitmapSource rgb, BitmapSource alpha)
    {
        // Put alpha back in
        WriteableBitmap dstW = new WriteableBitmap(new FormatConvertedBitmap(rgb, PixelFormats.Bgra32, null, 0));
        WriteableBitmap alphaW = new WriteableBitmap(alpha);
        unsafe
        {
            byte* resizedPtr = (byte*)dstW.BackBuffer.ToPointer();
            byte* alphaPtr = (byte*)alphaW.BackBuffer.ToPointer();
            for (int i = 0; i < dstW.PixelWidth * dstW.PixelHeight; i++)
                resizedPtr[i * 4 + 3] = alphaPtr[i];
        }

        return dstW;
    }

    private static BitmapSource GetScaledBitmap(BitmapSource src, ScaleTransform scale)
    {
        if (src.Format == PixelFormats.Bgra32) // special case when image has an alpha channel
        {
            // Put alpha in a gray bitmap and scale it
            BitmapSource alpha = GetAphaAsGrayBitmap(src);
            TransformedBitmap scaledAlpha = new TransformedBitmap(alpha, scale);

            // Scale RGB without taking in account alpha
            TransformedBitmap scaledSrc = new TransformedBitmap(new FormatConvertedBitmap(src, PixelFormats.Bgr32, null, 0), scale);

            // Merge them back
            return MergeAlphaAndRGB(scaledSrc, scaledAlpha);
        }
        else
        {
            return new TransformedBitmap(src, scale);
        }
    }