WPF - Modifying Image Colors on the Fly (C#)
Asked Answered
T

1

9

Is it possible to modify the colors of an Image in WPF via code (or even using Templates)?

Suppose I have an image which I need to apply to a Tile - which will have a White Foreground color by default and a Transparent Background. Something like the following PNG (it is somewhere here!):

Add New PNG

Instead of adding different images - with different colors, I just want to manipulate the White - and say change it to Black.

If it can be done, can someone give me a few pointers on what I need to do/look into.

Transcript answered 31/12, 2013 at 11:10 Comment(0)
T
10

One way to do this would be to use the BitmapDecoder class to retrieve the raw pixel data. You can then modify the pixels, and build a new WriteableBitmap from that modified pixel data:

// Copy pixel colour values from existing image.
// (This loads them from an embedded resource. BitmapDecoder can work with any Stream, though.)
StreamResourceInfo x = Application.GetResourceStream(new Uri(BaseUriHelper.GetBaseUri(this), "Image.png"));
BitmapDecoder dec = BitmapDecoder.Create(x.Stream, BitmapCreateOptions.None, BitmapCacheOption.Default);
BitmapFrame image = dec.Frames[0];
byte[] pixels = new byte[image.PixelWidth * image.PixelHeight * 4];
image.CopyPixels(pixels, image.PixelWidth*4, 0);

// Modify the white pixels
for (int i = 0; i < pixels.Length/4; ++i)
{
    byte b = pixels[i * 4];
    byte g = pixels[i * 4 + 1];
    byte r = pixels[i * 4 + 2];
    byte a = pixels[i * 4 + 3];

    if (r == 255 &&
        g == 255 &&
        b == 255 &&
        a == 255)
    {
        // Change it to red.
        g = 0;
        b = 0;

        pixels[i * 4 + 1] = g;
        pixels[i * 4] = b;
    }


}

// Write the modified pixels into a new bitmap and use that as the source of an Image
var bmp = new WriteableBitmap(image.PixelWidth, image.PixelHeight, image.DpiX, image.DpiY, PixelFormats.Pbgra32, null);
bmp.WritePixels(new Int32Rect(0, 0, image.PixelWidth, image.PixelHeight), pixels, image.PixelWidth*4, 0);
img.Source = bmp;

This works after a fashion, but there's a problem. Here's how the result looks if I show it on a dark background:

red cross on black background

As you can see, it's got a sort of white border. What's happened here is that your white cross had anti-aliased edges, meaning that the pixels around the edges are actually a semi-transparent shade of grey.

We can deal with that using a slightly more sophisticated technique in the pixel modification loop:

if ((r == 255 &&
        g == 255 &&
        b == 255 &&
        a == 255) ||
    (a != 0 && a != 255 &&
        r == g && g == b && r != 0))
{
    // Change it to red.
    g = 0;
    b = 0;

    pixels[i * 4 + 1] = g;
    pixels[i * 4] = b;
}

Here's how that looks on a black background:

red on black without white border

As you can see, that looks right. (OK, you wanted black not red, but the basic approach will be the same for any target colour.)

EDIT 2015/1/21 As ar_j pointed out in the comments, the Prgba format requires premultiplication. For the example I've given it is actually safe to ignore it, but if you were modifying colour channels in any way other than by setting them to 0, you'd need to multiple each value by (a/255). E.g., as aj_j shows for the G channel: pixels[i * 4 + 1] = (byte)(g * a / 255); Since g is zero in my code, this makes no difference but for non-primary colours you would need to do it that way.

Here it is on a gradient fill background just to show that the transparency is working:

red cross on gradient background

You could also write out the modified version:

var enc = new PngBitmapEncoder();
enc.Frames.Add(BitmapFrame.Create(bmp));
using (Stream pngStream = File.OpenWrite(@"c:\temp\modified.png"))
{
    enc.Save(pngStream);
}

Here's the result:

modified PNG

You can see the red cross, and it'll be on top of whatever background colour StackOverflow is using. (White, as I write this, but maybe they'll redesign one day.)

Whether this will work for the images you want to use is harder to know for certain, because it depends on what your definition of 'white' is - depending on how your images were produced, you may find things are ever so slightly off-white (particularly around the edges), and you may need further tweaking. But the basic approach should be OK.

Technician answered 31/12, 2013 at 14:40 Comment(5)
+1... Excellent info Ian thanks. However, I am storing the result back in an ImageSource, which I bind to with a Trigger on the Image.Style. The Background of the image is white. But when I save it to a file, it is still transparent. Not too sure what is wrong here. private ImageSource buttonBackSrcMouseOver = null; ... buttonBackSrcMouseOver = enc.Frames[0]; Will do a bit of googlingTranscript
Finally, this linked put the final piece in the jigsaw: #14162165Transcript
Are you sure the background of the image is really white in the UI, and that it's not just that the image is on top of a white background? (Leaving the transparent bits transparent was 'by design' in the code I showed. If you don't want that you could further modify the code that changes the pixel colour values.) Meanwhile, that linked question looks like it'll be overcomplicated for what you need. If your goal is to get an ImageSource you don't actually need to do anything - WritableBitmap already derives from ImageSource!Technician
PixelFormats.Pbgra32 expects you to pre-multiply the alpha value into the RGB channels, so you need to do that, otherwise non-black pixels will appear improperly with alpha < 255. e.g. pixels[i * 4 + 1] = (byte)(g * a / 255);Aprylapse
@ar_j bearing in mind that the value of g is 0 when that line runs, your modification is not needed in this case. The value of (g * a / 255) is 0. So in this particular case, the result is correct even without modification. (It only works because I'm leaving the R channel unaltered, and setting G and B to 0. If you wanted to plug in any other colour, or if you weren't starting with white to begin with, then yes, your modification would become necessary.)Technician

© 2022 - 2024 — McMap. All rights reserved.