How to change image’s color (RGB) at runtime in Windows 8

Problem

You have an image of unit (sprite) in your game and you would like to dynamically change selected colors. You would like to get units that will look the same for one player and different than others. Let’s say Player one has units with red shield, Player two uses blue color.

Red and blue dummy example:

You can use two different images prepared by designer but then your application size will be increased. If your unit’s graphic is 10KB and you support ten players with ten colors, you get 100KB size. Now, if your animation is in 8 different directions, you will have 800KB instead of 80KB and it’s just for one sprite!

RGB Solution

The most convenient way to do that (as far as I know) is to use ColorPalette class but unfortunately it’s not supported in new Windows 8. If we don’t have access to colors, we can only go through pixels and change them manually. :)

We don’t have SetPixel and GetPixel methods on Bitmap or WriteableBitmap classes so following example will NOT work:

Bitmap bitmap = LoadBitmap("Sprite.png");
for (int x = 0; x < bitmap.Width; x++)
{
    for (int y = 0; y < bitmap.Height; y++)
    {
        if (bitmap.GetPixel(x, y) == Color.Red)
        {
            bitmap.SetPixel(x, y, Color.Blue);
        }
    }
}

We have to go really deep and work on pixels directly. We will detect color to swap and replace pixel’s values.

XAML:
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel>
        <Button Content="Load image (Red to Cyan)" Click="Button_Click_1" />
        <Image x:Name="image" Stretch="None"/>
    </StackPanel>
</Grid>
Load Image and call “SwitchColors” method:
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
    FileOpenPicker picker = new FileOpenPicker();
    picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
    picker.FileTypeFilter.Add(".png");
    StorageFile file = await picker.PickSingleFileAsync();

    if (file != null)
    {
        using (IRandomAccessStream fileStream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read))
        {
            BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fileStream);

            PixelDataProvider pixelData = await decoder.GetPixelDataAsync(
                BitmapPixelFormat.Bgra8,
                BitmapAlphaMode.Straight,
                new BitmapTransform(),
                ExifOrientationMode.IgnoreExifOrientation,
                ColorManagementMode.DoNotColorManage);

            byte[] sourcePixels = pixelData.DetachPixelData();
            await ThreadPool.RunAsync(new WorkItemHandler(
                (IAsyncAction action) =>
                {
                    sourcePixels = SwitchColor(sourcePixels, decoder.PixelWidth, decoder.PixelHeight, Colors.Red, Colors.Cyan);
                }
                ));

            var writeableBitmap = new WriteableBitmap((int)decoder.PixelWidth, (int)decoder.PixelHeight);
            using (Stream stream = writeableBitmap.PixelBuffer.AsStream())
            {
                await stream.WriteAsync(sourcePixels, 0, sourcePixels.Length);
            }

            // Redraw the image
            image.Source = writeableBitmap;
        }
    }
}
Method “SwitchColors”:
private byte[] SwitchColor(byte[] sourcePixels, uint pixelWidth, uint pixelHeight, Color colorFrom, Color colorTo)
{
    int resultIndex = 0;

    // 4 bytes required for each pixel
    byte cFromB = colorFrom.B;
    byte cFromG = colorFrom.G;
    byte cFromR = colorFrom.R;
    byte cFromA = colorFrom.A;

    byte cToB = colorTo.B;
    byte cToG = colorTo.G;
    byte cToR = colorTo.R;
    byte cToA = colorTo.A;

    for (int y = 0; y < pixelHeight; y++)
    {
        for (int x = 0; x < pixelWidth; x++)
        {
            if (sourcePixels[resultIndex] == cFromG && sourcePixels[resultIndex + 1] == cFromB &&
                sourcePixels[resultIndex + 2] == cFromR && sourcePixels[resultIndex + 3] == cFromA)
            {
                sourcePixels[resultIndex] = cToB;
                sourcePixels[resultIndex + 1] = cToG;
                sourcePixels[resultIndex + 2] = cToR;
                sourcePixels[resultIndex + 3] = cToA;
            }
            resultIndex += 4;
        }
    }

    return sourcePixels;
}
Results

Image before and after (Red to Blue)

 

Sprite before and after (Red to Green and to Cyan)

I am changing only one single color. For our applications we will have some shadows and we would need to do that for at least few more (e.g. Red: {255, 255, 0, 0}, {255, 254, 0, 0}, {255, 253, 0, 0}).

There is also alternative to use HSL but that’s different story, not for today.

Optimization

If you know that Alpha channel will not change, don’s set it in the IF statement. You might also know that shield on the image starts on specific pixel {e.g. 300, 300}, so don’t iterate from 0 till end of the image.

Resources

  • Download source code: link

Thanks!

Tagged with: , , , ,
Posted in Tips&Tricks, Windows 8
10 comments on “How to change image’s color (RGB) at runtime in Windows 8
  1. koshik says:

    For the shades you could use a function to look at the distance from desired color:
    if (((R2-R1)^2 + (G2-G1)^2…) < limit) then Recolor(…);

    Then in Recolor you could just switch components (e.g. RG) or so. That shall be cheaper than HSL conversion and look as good.

  2. taz says:

    are you sure this works?
    when colorFrom is Colors.Red, in SwitchColor this breaks down to:

    colorFrom
    {#FFFF0000}
    A: 255
    B: 0
    G: 0
    R: 255

    however when i have a red pixel these are reported as:

    sourcePixels[0] = 36
    sourcePixels[1] = 28
    sourcePixels[2] = 237
    sourcePixels[3] = 255

    please advise?

    thanks.

  3. taz says:

    actually, ignore the last post – my sample image had dodgy colors!

  4. taz says:

    how would i set a pixel as transparent?

  5. taz says:

    ignore that last one as well.

  6. taz says:

    ok, heres a real question (lol).
    your code above does not seem to preserve transparency?
    i.e. if the original image has any transparent bits they are rendered as white on the output? hope you can help! thanks.

  7. taz says:

    grrr. i find im having to do as below. works, but feels wrong.

    byte cTransparentB = 255;
    byte cTransparentG = 255;
    byte cTransparentR = 255;
    byte cTransparentA = 0;

    for (int y = 0; y < pixelHeight; y++)
    {
    for (int x = 0; x < pixelWidth; x++)
    {
    if ((sourcePixels[resultIndex] == cFromG && sourcePixels[resultIndex + 1] == cFromB &&
    sourcePixels[resultIndex + 2] == cFromR && sourcePixels[resultIndex + 3] == cFromA) ||
    (sourcePixels[resultIndex] == cTransparentG && sourcePixels[resultIndex + 1] == cTransparentB &&
    sourcePixels[resultIndex + 2] == cTransparentR && sourcePixels[resultIndex + 3] == cTransparentA))
    {
    sourcePixels[resultIndex] = 0;
    sourcePixels[resultIndex + 1] = 0;
    sourcePixels[resultIndex + 2] = 0;
    sourcePixels[resultIndex + 3] = 0;
    }
    resultIndex += 4;
    }
    }

  8. mariusch says:

    Why are you loading the image? Is there no option to load the image from Assets?

  9. Mariusch says:

    How can I load a Image from Assets?

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>