Eugen
Eugen

Reputation: 2990

Transform 8bpp image into 24bpp and preserve color

I have an 8bpp image with a custom palete that holds a colored picture. enter image description here

Now, I'm trying to convert it to PixelFormat.Format24bppRgb format picture. I'm using direct pixels access using the code from here http://www.codeproject.com/Tips/240428/Work-with-bitmap-faster-with-Csharp

and the usage is

Bitmap bmp = (Bitmap) Image.FromFile("T:\\500-blue-orig.png");
LockBitmap lbmpSrc = new LockBitmap(bmp);
Bitmap dst = new Bitmap(bmp.Width, bmp.Height, PixelFormat.Format24bppRgb);
LockBitmap lbmpDst = new LockBitmap(dst);

lbmpSrc.LockBits();
lbmpDst.LockBits();

dst.Palette = bmp.Palette;
for (int y = 0; y < lbmpSrc.Height; y++)
{
    for (int x = 0; x < lbmpSrc.Width; x++)
    {
        Color c = lbmpSrc.GetPixel(x, y);

        lbmpDst.SetPixel(x, y, c);
    }
}

lbmpDst.UnlockBits();
lbmpSrc.UnlockBits();

dst.Save("T:\\x.png", ImageFormat.Png);

However the ending result is a grayscale image even though I do copy the original palette.

What am I doing wrong here? How do I get a 24bpp colored image from a 8bpp picture which actually has colors?

Upvotes: 2

Views: 1864

Answers (3)

Nyerguds
Nyerguds

Reputation: 5629

People here seem to all be ridiculously overcomplicating matters. Converting an 8BPP image to 24BPP or 32BPP doesn't need any special code.

The only thing that's difficult about 8BPP images is manipulating them, and, you don't need to do that at all; your end result isn't one of these problematic 8BPP images.

You can do it in less than five lines:

public static Bitmap PaintOn32bpp(Image image)
{
    Bitmap bp = new Bitmap(image.Width, image.Height, PixelFormat.Format24bppRgb);
    using (Graphics gr = Graphics.FromImage(bp))
        gr.DrawImage(image, new Rectangle(0, 0, bp.Width, bp.Height));
    return bp;
}

This will work with any image, regardless of its colour format.

Upvotes: 0

Bas
Bas

Reputation: 27095

I tried a manual approach using unmanaged code - local benchmark shows it to be 99.7% faster than the ignorant (Bitmap.GetPixel > Bitmap.SetPixel) approach.

Basically, we use the LockBits pointer and assign bytes one by one based on the color palette.

static unsafe void To24Bpp(Bitmap source, Bitmap dest)
{
    var sourceData = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly,
        PixelFormat.Format8bppIndexed);
    var destData = dest.LockBits(new Rectangle(0, 0, dest.Width, dest.Height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
    var paletteBytes = source.Palette.Entries.Select(ColorToUintRgbLeftAligned).ToArray();
    var current = (byte*) sourceData.Scan0.ToPointer();
    var lastPtr = (byte*) (sourceData.Scan0 + sourceData.Width*sourceData.Height).ToPointer();
    var targetPtr = (byte*) destData.Scan0;
    while (current <= lastPtr)
    {
        var value = paletteBytes[*current++];
        targetPtr[0] = (byte) (value >> 24);
        targetPtr[1] = (byte) (value >> 16);
        targetPtr[2] = (byte) (value >> 8);
        targetPtr += 3;
    }

    source.UnlockBits(sourceData);
    dest.UnlockBits(destData);
}

static uint ColorToUintRgbLeftAligned(Color color)
{
    return ((uint) color.B << 24) + ((uint) color.G << 16) + ((uint) color.R << 8);
}

The code could be improved to write 4 bytes at a time from the color pallette, reducing the amount of random memory access. My local benchmark showed the performance of this improved by a further 25%. Note the difference in building the uint color bytes - the alignment of bytes in a uint was opposite of what I expected.

private static unsafe void To24BppUintAssignment(Bitmap source, Bitmap dest)
{
    var sourceData = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed);
    var destData = dest.LockBits(new Rectangle(0, 0, dest.Width, dest.Height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
    uint[] paletteBytes = source.Palette.Entries.Select(ColorToUintRgbRightAligned).ToArray();
    var current = (byte*)sourceData.Scan0.ToPointer();
    var lastPtr = (byte*)(sourceData.Scan0 + sourceData.Width * sourceData.Height).ToPointer();
    var targetPtr = (byte*) destData.Scan0;
    while (current < lastPtr)
    {
        var targetAsUint = ((uint*) targetPtr);
        targetAsUint[0] = paletteBytes[*current++];
        targetPtr += 3;
    }
    uint finalValue = paletteBytes[*current];
    targetPtr[0] = (byte)(finalValue >> 24);
    targetPtr[1] = (byte)(finalValue >> 16);
    targetPtr[2] = (byte)(finalValue >> 8);
        source.UnlockBits(sourceData);
    dest.UnlockBits(destData);
    }
    private static uint ColorToUintRgbRightAligned(Color color)
    {
        return ((uint)color.B) + ((uint)color.G << 8) + ((uint)color.R << 16);
    }

I didn't create the bitmap in the method for benchmarking purposes, it should be called as such:

static Bitmap To24Bpp(Bitmap source)
{
    var dest = new Bitmap(source.Width, source.Height, PixelFormat.Format24bppRgb);
    To24BppUintAssignment(source, dest);
    return dest;
}

Upvotes: 3

Sami Kuhmonen
Sami Kuhmonen

Reputation: 31153

The LockBitmap class you are using doesn't care about palette, it assumes 8bpp images are always grayscale and will return only grays.

Also the class is far from fast since it copies bitmap data to another array and back, creates Color when not necessarily needed etc. If you really want performance you will do the handling yourself.

You have two choices:

  • use GetPixel and SetPixel from Bitmap directly. It will work as it should.

  • copy the 8bpp palette image into a 32/24bpp image first, then use that class for processing

Upvotes: 1

Related Questions