在 ASP.NET C# 中,我试图将位图图像另存为 16 色非透明灰度图像作为 PNG 或 GIF。我假设我必须创建一个调色板,然后以某种方式将调色板附加到图像上,但不知道如何去做。
源图像是 24 位彩色位图。
这叫做量化,它很复杂。我已经广泛地解决了这个问题,我最好的结果是使用八叉树量化和自定义扩散算法。
从 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
通读评论并根据我的评论修补指针算术错误....
但是,没有抖动,您可能无法在不完全信任的环境中运行原件。这些年来我做了很多补丁,但我不记得它们了。
如果您不介意浏览一堆开源代码,另一种可能性是下载 Paint.Net。我相信它可以转换为灰度,但我可能错了,因为我需要使用它已经有一段时间了。
这实际上一点也不难,一旦你有了工具集,我就建立了很多工具集。你需要的东西是:
调色板很简单。灰度值是红色、绿色和蓝色具有相同值的颜色,并且对于 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;
}