在ASP.NET C#中,我试图将位图图像保存为16色非透明灰度图像,作为PNG或GIF。我假设我必须创建一个调色板,然后以某种方式将调色板附加到图像,但不知道如何去做。
源图像是24位彩色位图。
答案 0 :(得分:2)
这叫做量化,而且很复杂。我已经广泛地解决了这个问题,我最好的结果是使用八叉树量化和自定义扩散算法。
从A到B的最快点是grab my code (open-source, but $69 to download),并使用极其简单的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
阅读评论并根据我的评论修补指针算术错误....
但是,没有抖动,您可能无法在不完整的信任环境中运行原始文件。多年来我已经做了很多补丁,我不记得这些。答案 1 :(得分:1)
如果你不介意浏览一堆开源代码,另一种可能性就是下载Paint.Net。我相信它可以转换为灰度,但我可能会错,因为我已经有一段时间了,因为我需要使用它。
答案 2 :(得分:0)
这实际上并不难,一旦你获得了工具集,我构建了相当多的工具集。你需要的是:
调色板很简单。灰度值是红色,绿色和蓝色相同的颜色,16色的颜色之间的亮度相等,该值只是0x00,0x11,0x22等到0xFF的范围。不应该很难。
下一步是将图像颜色与调色板颜色相匹配,并生成这些值的字节数组。有几种方法可以在stackoverflow上获得最接近的匹配。这个问题有很多:
How to compare Color object and get closest Color in an Color[]?
接下来是棘手的部分:将实际图像数据转换为4位。
要记住的一件事是每行保存图像,这样的线(称为“扫描线”)不一定与图像的宽度相同。例如,在每像素4位中,每个字节可以容纳2个像素,因此逻辑上,步幅是宽度除以2.但是,如果宽度是一个不均匀的数字,则每一行的末尾都会有一个字节。只有半满。系统不会将下一行的第一个像素放在那里;相反,它只是留空。对于8位或甚至16位图像,我知道步幅经常将扫描线对齐为4个字节的多个。所以永远不要假设宽度与扫描线长度相同。
对于我在本回复中进一步说明的功能,我使用了所需的最小扫描线长度。由于这只是比特长度除以8的宽度乘以倍加一,如果在该除法中有余数,则可以很容易地计算为((bpp * width) + 7) / 8
。
现在,如果您生成了灰度调色板,然后为图像上的每个像素创建了一个包含最近调色板值的字节数组,则可以将所有值提供给实际的8位到4位转换函数。 / p>
我写了一个函数将8位数据转换为任何给定的位长度。因此,对于您的4位图像,这需要bitsLength=4
。
BigEndian参数将确定是否切换一个字节内的值。我不确定.Net图像在这里,但我知道很多1BPP格式都使用big-endian位,而我遇到的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"></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;
}
最后,复制内存使用的功能。如您所见,此方法使用作为参数给出的步幅逐行复制,因此可以忽略.Net框架创建的Bitmap
使用的内部步幅。无论如何它都会相同或更大。
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);
}
}