1

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

源图像是 24 位彩色位图。

4

3 回答 3

2

这叫做量化,它很复杂。我已经广泛地解决了这个问题,我最好的结果是使用八叉树量化和自定义扩散算法。

从 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-善良.aspx

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

但是,没有抖动,您可能无法在不完全信任的环境中运行原件。这些年来我做了很多补丁,但我不记得它们了。

于 2010-01-04T21:50:47.827 回答
1

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

于 2010-01-04T23:01:41.043 回答
0

这实际上一点也不难,一旦你有了工具集,我就建立了很多工具集。你需要的东西是:

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

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

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

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

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

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

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

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

我编写了一个函数来将 8 位数据转换为任何给定的位长度。所以这需要bitsLength=4你的 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;
    }
于 2017-03-31T13:33:00.937 回答