2010-01-04 65 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); 
    } 
}