如何在 ASP.NET 中将位图另存为 16 色灰度 GIF 或 PNG?

发布于 2024-08-17 11:04:55 字数 121 浏览 2 评论 0原文

在 ASP.NET C# 中,我尝试将位图图像另存为 16 色不透明灰度图像(PNG 或 GIF)。我假设我必须创建一个调色板,然后以某种方式将调色板附加到图像上,但不知道如何去做。

源图像是 24 位颜色位图。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

遗失的美好 2024-08-24 11:04:55

这就是所谓的量化,而且很复杂。我对这个问题进行了广泛的研究,我的最佳结果是使用八叉树量化和自定义扩散算法。

从 A 到 B 的最快点是 获取我的代码(开源,但 69 美元)下载),并使用极其简单的 API 将颜色数设置为 16 并保存为 GIF 或 PNG。如果您想通过代码隐藏来执行此操作,则应该大约 2 行代码...或者,如果它位于文件系统上,则可以使用查询字符串:

image.bmp?format=gif&colors=16

如果图像还不是灰度图像,您可以使用 ImageAttributes 类来执行此操作模块的。生成的 GIF 将自动具有灰度调色板。最少的工作,最大的成果。

请记住,您不必将其用作 HttpModule - 它主要是一个用于调整图像大小、修改和编码图像的库。

如果你想自己动手,这就是我的开始:
http://codebetter.com/blogs/brendan.tompkins/archive/2007/06/14/gif-image-color-quantizer-now-with-safe-goodness.aspx

阅读评论并根据我的评论修补指针算术错误......

不过,没有抖动,并且您可能会在低于完全信任的环境中运行原始版本时遇到问题。多年来我制作了很多补丁,但我不记得全部了。

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.

陌伤浅笑 2024-08-24 11:04:55

如果您不介意浏览一堆开源代码,另一种可能性是下载 Paint.Net。我相信它可以转换为灰度,但我可能是错的,因为我已经有一段时间没有需要使用它了。

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.

兔姬 2024-08-24 11:04:55

一旦你获得了工具集,这实际上一点也不难,而且我已经建立了很多这样的工具集。您需要的是:

  • 16 色灰度调色板。
  • 将图像数据与最接近的颜色进行匹配的函数(以获取调色板数据)
  • 将这些匹配转换为 4 位数据的函数(每个值半个字节)
  • 将该数据写入新的 4 位图像对象的方法。

调色板很简单。灰度值是红、绿、蓝具有相同值的颜色,对于 16 种颜色的颜色之间的相同亮度步长,该值只是从 0x00、0x11、0x22 等到 0xFF 的范围。应该不难做。

下一步是将图像颜色与调色板颜色进行匹配,并创建这些值的字节数组。 stackoverflow 上已经有多种方法可以获取最接近的匹配。这个问题有很多:

如何比较 Color 对象并获得 Color[] 中最接近的 Color?

接下来是棘手的部分:将实际图像数据转换为 4 位。

要记住的一件事是,图像是按行保存的,这样的线(称为“扫描线”)不一定与图像的宽度相同。例如,在每像素 4 位中,每个字节可以容纳 2 个像素,因此从逻辑上讲,步长是宽度除以 2。但是,如果宽度是奇数,则每行末尾都会有一个字节,即只填了一半。系统不会将下一行的第一个像素放在那里;相反,它只是留空。对于 8 位甚至 16 位图像,我知道步幅通常会将扫描线对齐到 4 字节的倍数。因此,永远不要假设宽度与扫描线长度相同。

对于我在此回复中进一步放置的功能,我使用所需的最小扫描线长度。由于这只是宽度乘以位长度除以八,如果该除法中有余数则加一,因此可以轻松计算为 ((bpp * width) + 7) / 8

现在,如果您生成了灰度调色板,然后创建了一个包含图像上每个像素最接近的调色板值的字节数组,则您可以将所有值提供给实际的 8 位到 4 位转换函数。

我编写了一个函数来将 8 位数据转换为任何给定的位长度。因此,您的 4 位图像需要 bitsLength=4

BigEndian 参数将决定一个字节内的值是否交换。我不确定这里的 .Net 图像,但我知道很多 1BPP 格式使用大端位,而我遇到过以最低半字节开头的 4BPP 格式。

    /// <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;
}

下一步是制作正确尺寸和像素格式的图像,在内存中打开其支持数组,并将数据转储到其中。 16 色图像的像素格式为 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">Pixel format.</param>
    /// <param name="palette">Color palette.</param>
    /// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
    /// <returns>The new image.</returns>
    public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
    {
        Bitmap newImage = new Bitmap(width, height, pixelFormat);
        BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
        Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
        Int32 targetStride = targetData.Stride;
        Int64 scan0 = targetData.Scan0.ToInt64();
        for (Int32 y = 0; y < height; ++y)
            Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
        newImage.UnlockBits(targetData);
        // For indexed images, set the palette.
        if ((pixelFormat & PixelFormat.Indexed) != 0 && (palette != null || defaultColor.HasValue))
        {
            if (palette == null)
                palette = new Color[0];
            ColorPalette pal = newImage.Palette;
            Int32 palLen = pal.Entries.Length;
            Int32 paletteLength = palette.Length;
            for (Int32 i = 0; i < palLen; ++i)
            {
                if (i < paletteLength)
                    pal.Entries[i] = palette[i];
                else if (defaultColor.HasValue)
                    pal.Entries[i] = defaultColor.Value;
                else
                    break;
            }
            // Palette property getter creates a copy, so the newly filled in palette
            // is not actually referenced in the image until you set it again explicitly.
            newImage.Palette = pal;
        }
        return newImage;
    }

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">Pixel format.</param>
    /// <param name="palette">Color palette.</param>
    /// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
    /// <returns>The new image.</returns>
    public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
    {
        Bitmap newImage = new Bitmap(width, height, pixelFormat);
        BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
        Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
        Int32 targetStride = targetData.Stride;
        Int64 scan0 = targetData.Scan0.ToInt64();
        for (Int32 y = 0; y < height; ++y)
            Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
        newImage.UnlockBits(targetData);
        // For indexed images, set the palette.
        if ((pixelFormat & PixelFormat.Indexed) != 0 && (palette != null || defaultColor.HasValue))
        {
            if (palette == null)
                palette = new Color[0];
            ColorPalette pal = newImage.Palette;
            Int32 palLen = pal.Entries.Length;
            Int32 paletteLength = palette.Length;
            for (Int32 i = 0; i < palLen; ++i)
            {
                if (i < paletteLength)
                    pal.Entries[i] = palette[i];
                else if (defaultColor.HasValue)
                    pal.Entries[i] = defaultColor.Value;
                else
                    break;
            }
            // Palette property getter creates a copy, so the newly filled in palette
            // is not actually referenced in the image until you set it again explicitly.
            newImage.Palette = pal;
        }
        return newImage;
    }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文