2010-01-04 89 views
1

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

源图像是一个24位彩色位图。

回答

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

通读意见和按我的意见修补指针运算错误....

无抖动,不过,并且在不到完全信任的环境中运行原始文件时可能会遇到问题。多年来我做了很多补丁,我都不记得了。

+0

查看codeplex.com和code.msdn.com获取更多免费示例,包括八叉树和其他算法。 – 2010-01-04 22:38:39

+2

什么样的“开源”软件需要69美元才能下载?!? – TAG 2010-01-05 04:10:03

+0

@不退款 - 是的,MSDN上有一些,但它们比codebetter.com更老,更笨。如果你阅读这篇文章,那就是他的开始。 @TAG许多开源软件。像许多Linux发行版一样。作为另一个项目的一部分,它是可重新分发的开源软件,而不是免费的。我讨厌许可证麻烦,我认为其他开发人员也这样做。 – 2010-01-05 13:33:34

1

另一种可能性,如果你不介意在一堆开源代码中拖动,那就下载Paint.Net。我相信它可以转换为灰度,但是我可能会错,因为我有一段时间需要使用它。

0

这实际上并不难,一旦你得到了工具集,并且我建立了其中的一些。你需要的东西是:

  • 一个16色的灰度调色板。
  • 以匹配图像数据到最近的颜色的函数(以获得调色板数据)
  • 的函数这些匹配至4比特数据(每个值半个字节)
  • A至该数据写入方式转换一个新的4位图像对象。

调色板很简单。灰度值是红色,绿色和蓝色具有相同值的颜色,并且对于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的宽度乘以1,如果在该除法中存在余数,则可以容易地计算为((bpp * width) + 7)/8

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

我写了一个函数将8位数据转换为任意给定的位长。所以这将需要bitsLength=4为您的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); 
    } 
}