Search code examples
c#bitmapcolor-palette

Is there a way of extracting the index of a pixel in an indexed colour Bitmap (C#)?


I've loaded an indexed colour image (8bppI) with a unique palette into a C# program and I need to access the index of colours in that image. However, the only access function seems to be Bitmap.GetPixel(x,y) which returns a colour, not an index. When that same colour is inserted back into a Bitmap of the same format and palette, the colour information is apparently misinterpreted as an index and everything goes to heck. Here's a simplified version of the code for clarity of the issue:

    public void CreateTerrainMap() {
        visualization = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
        visualizationLock = new LockBitmap(visualization);
        Lock();

        // "TerrainIndex.bmp" is a 256x256 indexed colour image (8bpp) with a unique palette.
        // Debugging confirms that this image loads with its palette and format intact
        Bitmap terrainColours = new Bitmap("Resources\\TerrainIndex.bmp");
        visualization.Palette = terrainColours.Palette;

        Color c;

        for (int x = 0; x < width; x++) { 
            for (int y = 0; y < height; y++) {
                if (Terrain[x, y] < SeaLevel) {
                    c = Color.FromArgb(15); // Counterintuitively, this actually gives index 15 (represented as (0,0,0,15))
                } else {
                    heatIndex = <some number between 0 and 255>;
                    rainIndex = <some number between 0 and 255>;

                    if (IsCoastal(x, y)) {
                        c = Color.FromArgb(35); // Counterintuitively, this actually gives index 35 (represented as (0,0,0,35))
                    } else {
                        // This returns an argb colour rather than an index...
                        c = terrainColours.GetPixel(rainIndex, heatIndex);
                    }
                }
                // ...and this seemingly decides that the blue value is actually an index and sets the wrong colour entirely
                visualizationLock.SetPixel(x, y, c);
            }
        }
    }

TerrainIndex looks like this: TerrainIndex.bmp

The palette looks like this: Palette

The output should look like this: Good

But it looks like this instead: Bad

Note that the oceans (index 15) and coasts (index 35) look correct, but everything else is coming from the wrong part of the palette.

I can't find any useful information on working with indexed colour bitmaps in C#. I really hope someone can explain to me what I might be doing wrong, or point me in the right direction.


Solution

  • I created an answer from my comment. So the "native" solution is something like this (requires allowing unsafe code):

    Bitmap visualization = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
    visualization.Palette = GetVisualizationPalette();
    BitmapData visualizationData = visualization.LockBits(new Rectangle(Point.Empty, visualization.Size),
        ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
    try
    {
        unsafe
        {
            byte* row = (byte*)visualizationData.Scan0;
            for (int y = 0; y < visualizationData.Height; y++)
            {
                for (int x = 0; x < visualizationData.Width; x++)
                {
                    // here you set the 8bpp palette index directly
                    row[x] = GetHeatIndex(x, y);
                }
    
                row += visualizationData.Stride;
            }
        }
    }
    finally
    {
        visualization.UnlockBits(visualizationData);
    }
    

    Or, you can use these libraries, and then:

    using KGySoft.Drawing;
    using KGySoft.Drawing.Imaging;
    
    // ...
    
    Bitmap visualization = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
    visualization.Palette = GetVisualizationPalette();
    using (IWritableBitmapData visualizationData = visualization.GetWritableBitmapData())
    {
        for (int y = 0; y < visualizationData.Height; y++)
        {
            IWritableBitmapDataRow row = visualizationData[y];
            for (int x = 0; x < visualizationData.Width; x++)
            {
                // setting pixel by palette index
                row.SetColorIndex(x, GetHeatIndex(x, y));
    
                // or: directly by raw data (8bpp means 1 byte per pixel)
                row.WriteRaw<byte>(x, GetHeatIndex(x, y));
    
                // or: by color (automatically applies the closest palette index)
                row.SetColor(x, GetHeatColor(x, y));
            }
        }
    }
    

    Edit: And for reading pixels/indices you can use terrainColors.GetReadableBitmapData() so you will able to use rowTerrain.GetColorIndex(x) or rowTerrain.ReadRaw<byte>(x) in a very similar way.