Question

In ASP.NET C# I'm trying to save a bitmap image as an 16-color non-transparent grayscale image as either a PNG or GIF. I assume I have to create a palette and then somehow attach the palette to the image but not sure how to go about doing this.

The source image is a 24-bit color Bitmap.

Was it helpful?

Solution

It's called quantization, and it's complicated. I've worked extensively with this problem, and my best results have been using Octree quantization and a custom diffusion algorithm.

Your fastest point from A to B is grab my code (open-source, but $69 to download) and use the extremely simple API to set the color count to 16 and save as GIF or PNG. Should be about 2 lines of code if you want to do it via code-behind... or, you can use a querystring if it's on the filesystem:

image.bmp?format=gif&colors=16

If the image isn't already grayscale, you can do that using the ImageAttributes class of the module. The resulting GIF will automatically have a grayscale palette. Minimal work, great results.

Remember you don't have to use it as an HttpModule - it's primarily a library for resizing, modifying, and encoding images.

If you want to roll your own, here's what I started with: http://codebetter.com/blogs/brendan.tompkins/archive/2007/06/14/gif-image-color-quantizer-now-with-safe-goodness.aspx

Read through the comments and patch the pointer arithmetic errors per my comments....

No dithering, though, and you may have trouble running the original in less than a full trust environment. I've made a lot of patches over the years, and I don't remember them all.

OTHER TIPS

Another possibility if you don't mind trolling through a bunch of open source code is to download Paint.Net. I believe it can convert to Grayscale, but I could be wrong as it's been a while since I've had a need to use it.

This is actually not hard at all, once you got the toolsets, and I built up quite a few of those. The things you need are:

  • A 16-color grayscale palette.
  • A function to match image data to the nearest color (to get paletted data)
  • A function to convert these matches to 4-bit data (half a byte per value)
  • A way to write that data into a new 4-bit image object.

The palette is easy. Gray values are colors with the same value for red, green and blue, and for equal brightness steps between colours on 16 colour, that value is just the range from 0x00, 0x11, 0x22 etc up to 0xFF. Shouldn't be hard to make.

The next step is matching the image colours to the palette colours, and making a byte array of these values. There are several methods for getting the closest match available on stackoverflow already. This question has a bunch of them:

How to compare Color object and get closest Color in an Color[]?

Next comes the tricky part: converting the actual image data to 4-bit.

One thing to keep in mind is that images are saved per line, and such a line (called a "scanline") isn't necessarily the same width as the image. For example, in 4 bits per pixel, you can fit 2 pixels in each byte, so logically, the stride is width divided by 2. However, if the width is an uneven number, each line will have a byte at the end that is only half-filled. The systems do not put the first pixel of the next line in there; instead it's simply left blank. And for 8-bit or even 16-bit images I know the stride often aligns the scanlines to multiple of 4 bytes. So never assume the width is the same as the scanline length.

For the function I put further down in this reply I use the minimum needed scanline length. Since this is just the width times times the bits length divided by eight, plus one if there was a remainder in that division, it can easily be calculated as ((bpp * width) + 7) / 8.

Now, if you generated your greyscale palette, and then made a byte array containing the nearest palette value for each pixel on the image, you have all values to feed to the actual 8-bit to 4-bit conversion function.

I wrote a function to convert 8-bit data to any given bits length. So this would need bitsLength=4 for your 4-bit image.

The BigEndian parameter will determine whether the values inside one byte are switched or not. I'm not sure about .Net images here, but I know a lot of 1BPP formats use big-endian bits, while I've encountered 4BPP formats that started with the lowest nibble instead.

    /// <summary>
    /// Converts given raw image data for a paletted 8-bit image to lower amount of bits per pixel.
    /// </summary>
    /// <param name="data8bit">The eight bit per pixel image data</param>
    /// <param name="width">The width of the image</param>
    /// <param name="height">The height of the image</param>
    /// <param name="newBpp">The new amount of bits per pixel</param>
    /// <param name="stride">Stride used in the original image data. Will be adjusted to the new stride value.</param>
    /// <param name="bigEndian">Values inside a single byte are read from the largest to the smallest bit.</param>
    /// <returns>The image data converted to the requested amount of bits per pixel.</returns>
private static Byte[] ConvertFrom8Bit(Byte[] data8bit, Int32 width, Int32 height, Int32 bitsLength, Boolean bigEndian)
    {
        if (newBpp > 8)
            throw new ArgumentException("Cannot convert to bit format greater than 8!", "newBpp");
        if (stride < width)
            throw new ArgumentException("Stride is too small for the given width!", "stride");
        if (data8bit.Length < stride * height)
            throw new ArgumentException("Data given data is too small to contain an 8-bit image of the given dimensions", "data8bit");
    Int32 parts = 8 / bitsLength;
    // Amount of bytes to write per width
    Int32 stride = ((bpp * width) + 7) / 8;
    // Bit mask for reducing original data to actual bits maximum.
    // Should not be needed if data is correct, but eh.
    Int32 bitmask = (1 << bitsLength) - 1;
    Byte[] dataXbit = new Byte[stride * height];
    // Actual conversion porcess.
    for (Int32 y = 0; y < height; y++)
    {
        for (Int32 x = 0; x < width; x++)
        {
            // This will hit the same byte multiple times
            Int32 indexXbit = y * stride + x / parts;
            // This will always get a new index
            Int32 index8bit = y * width + x;
            // Amount of bits to shift the data to get to the current pixel data
            Int32 shift = (x % parts) * bitsLength;
            // Reversed for big-endian
            if (bigEndian)
                shift = 8 - shift - bitsLength;
            // Get data, reduce to bit rate, shift it and store it.
            dataXbit[indexXbit] |= (Byte)((data8bit[index8bit] & bitmask) << shift);
        }
    }
    return dataXbit;
}

The next step is to make an image of the correct dimensions and pixel format, open its backing array in memory, and dump your data into it. The pixel format for a 16 colour image is PixelFormat.Format4bppIndexed.

/// <summary>
/// Creates a bitmap based on data, width, height, stride and pixel format.
/// </summary>
/// <param name="sourceData">Byte array of raw source data</param>
/// <param name="width">Width of the image</param>
/// <param name="height">Height of the image</param>
/// <param name="stride">Scanline length inside the data</param>
/// <param name="pixelFormat"></param>
/// <param name="palette">Color palette</param>
/// <returns>The new image</returns>
public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette)
{
    if (width == 0 || height == 0)
        return null;
    Bitmap newImage = new Bitmap(width, height, pixelFormat);
    BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
    CopyMemory(targetData.Scan0, sourceData, sourceData.Length, stride, targetData.Stride);
    newImage.UnlockBits(targetData);
    // For 8-bit images, set the palette.
    if ((pixelFormat == PixelFormat.Format8bppIndexed || pixelFormat == PixelFormat.Format4bppIndexed) && palette != null)
    {
        ColorPalette pal = newImage.Palette;
        for (Int32 i = 0; i < pal.Entries.Length; i++)
            if (i < palette.Length)
            pal.Entries[i] = palette[i];
        newImage.Palette = pal;
    }
    return newImage;
}

And finally, the functions to copy memory used by that. As you can see, this method specifically copies line by line, using the stride given as argument, so the internal stride used by the Bitmap that the .Net framework creates can be ignored. It will be either the same or larger anyway.

public static void CopyMemory(IntPtr target, Byte[] sourceBytes, Int32 length, Int32 origStride, Int32 targetStride)
{
    IntPtr unmanagedPointer = Marshal.AllocHGlobal(sourceBytes.Length);
    Marshal.Copy(sourceBytes, 0, unmanagedPointer, sourceBytes.Length);
    CopyMemory(target, unmanagedPointer, length, origStride, targetStride);
    Marshal.FreeHGlobal(unmanagedPointer);
}

public static void CopyMemory(IntPtr target, IntPtr source, Int32 length, Int32 origStride, Int32 targetStride)
{
    IntPtr sourcePos = source;
    IntPtr destPos = target;
    Int32 minStride = Math.Min(origStride, targetStride);
    Byte[] imageData = new Byte[targetStride];
    while (length >= origStride && length > 0)
    {
        Marshal.Copy(sourcePos, imageData, 0, minStride);
        Marshal.Copy(imageData, 0, destPos, targetStride);
        length -= origStride;
        sourcePos = new IntPtr(sourcePos.ToInt64() + origStride);
        destPos = new IntPtr(destPos.ToInt64() + targetStride);
    }
    if (length > 0)
    {
        Marshal.Copy(sourcePos, imageData, 0, length);
        Marshal.Copy(imageData, 0, destPos, length);
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top