Search code examples
c#performancecolorsbitmapbit-manipulation

Is there a way to convert 16-bit color to 24-bit color efficiently while avoiding floating-point math?


I'm decoding a .BMP file, and I'm at a point where I need to handle 16-bit colors. The entire codebase uses 32-bit colors (R, G, B, A), so I need to convert the color to a 24-bit RGB value (one byte for each color).

Each component of the color is 5-bit as per the specification (1 bit is wasted). My code is as follows:

ushort color = BitConverter.ToUInt16(data, 54 + i);
byte blue  = (byte)((color | 0b0_00000_00000_11111) / 31f * 255);
byte green = (byte)(((color | 0b0_00000_11111_00000) >> 5) / 31f * 255);
byte red   = (byte)(((color | 0b0_11111_00000_00000) >> 10) / 31f * 255);

However, this doesn't seem particularly efficient. I tried doing color << (8 - 5), which makes the process much faster and avoids floating-point conversions, but it isn't accurate - a value of 31 (11111) converts to 248. Is there a way to achieve this with some other bit-manipulation hack, or am I forced to convert each number to a float just to change the color space?


Solution

  • Not only the floating point conversions can be avoided but also multiplications and divisions. From my implementation:

    internal struct Color16Rgb555
    {
        private const ushort redMask = 0b01111100_00000000;
        private const ushort greenMask = 0b00000011_11100000;
        private const ushort blueMask = 0b00011111;
    
        private ushort _value;
    
        internal Color16Rgb555(ushort value) => _value = value;
    
        internal byte R => (byte)(((_value & redMask) >> 7) | ((_value & redMask) >> 12));
        internal byte G => (byte)(((_value & greenMask) >> 2) | ((_value & greenMask) >> 7));
        internal byte B => (byte)(((_value & blueMask) << 3) | ((_value & blueMask) >> 2));
    }
    

    Usage:

    var color = new Color16Rgb555(BitConverter.ToUInt16(data, 54 + i));
    byte blue  = color.B;
    byte green = color.G;
    byte red   = color.R;
    

    It produces 255 for 31 because it fills up the remaining 3 bits with the 3 MSB bits of the actual 5 bit value.

    But assuming your data is a byte array you have an even more convenient option if you use my drawing libraries:

    // to interpret your data array as 16BPP pixels with RGB555 format:
    var my16bppBitmap = BitmapDataFactory.CreateBitmapData(
        data, // your back buffer
        new Size(pixelWidth, pixelHeight), // size in pixels
        stride, // the size of one row in bytes
        KnownPixelFormat.Format16bppRgb555);
    
    // now you can get/set pixels normally    
    Color somePixel = my16bppBitmap.GetPixel(0, 0);
    
    // For better performance obtain a row first.
    var row = my16bppBitmap[0]; // or FirstRow (+MoveNextRow if you wish)
    Color32 asColor32 = row[0]; // accessing pixels regardless of PixelFormat
    ushort asUInt16 = row.ReadRaw<ushort>(0); // if you know that it's a 16bpp format