使用 BitmapSource、PngBitmapEncoder/Decoder 时,在 PNG 中保存 8 位索引数据会改变原始字节

Posted

技术标签:

【中文标题】使用 BitmapSource、PngBitmapEncoder/Decoder 时,在 PNG 中保存 8 位索引数据会改变原始字节【英文标题】:Saving 8-bit indexed data in a PNG alters original bytes when using BitmapSource, PngBitmapEncoder/Decoder 【发布时间】:2012-08-31 15:06:29 【问题描述】:

我在使用 BitmapSource 和 PngBitmapEncoder/Decoder 保存和加载 PNG 时遇到问题。基本上,我希望能够保存一个源自字节数组的图像,并且当 PNG 加载到我的程序中时,重新加载完全相同的字节。原始数据的保存很重要。

同时,我希望 PNG 使用自定义调色板(256 色的索引数组)。

我正在尝试使用自定义索引调色板保存我的 8 位数据。原始数据的范围为 0-255。调色板可以是“阈值”调色板(例如,0-20 是颜色 #1,21-50 是颜色 #2,等等)。

我发现,当我保存数据、重新加载并执行 CopyPixels 以检索“原始”数据时,数据值是根据调色板设置的,而不是原始字节数组值。

有没有办法保留 PNG 中的原始字节数组, 不会丢失自定义调色板?还是有其他方法可以从 BitmapSource 中检索字节数组?

以下是我的保存程序:

      // This gets me a custom palette that is an array of 256 colors
      List<System.Windows.Media.Color> colors = PaletteToolsWPF.TranslatePalette(this, false, true);
      BitmapPalette myPalette = new BitmapPalette(colors);

      // This retrieves my byte data as an array of dimensions _stride * sizeY
      byte[] ldata = GetData();

      BitmapSource image = BitmapSource.Create(
        sizeX,
        sizeY,
        96,
        96,
        PixelFormats.Indexed8,
        myPalette,
        ldata,
        _stride);

      PngBitmapEncoder enc = new PngBitmapEncoder();
      enc.Interlace = PngInterlaceOption.On;
      enc.Frames.Add(BitmapFrame.Create(image));

      // save the data via FileStream
      enc.Save(fs);

这是我的加载程序:

    // Create an array to hold the raw data
    localData = new byte[_stride * sizeY];

    // Load the data via a FileStream
    PngBitmapDecoder pd = new PngBitmapDecoder(Fs, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
    BitmapSource bitmapSource = pd.Frames[0];    

    // When I look at the byte data, it is *not* the same as my original data 
    bitmapSource.CopyPixels(localData, _stride, 0);

任何建议将不胜感激。谢谢。

附录 #1:我发现部分问题在于 PNG 被保存为 32 位颜色,尽管我将它设置为 Indexed8 并使用 256 条目的调色板。这似乎也取决于设置的调色板。知道为什么吗?

【问题讨论】:

请用相关语言(C#)标记? 我对 PngBitmapEncoder 不是很熟悉,但是:PngCs 提供了对 png 解码/编码的高度控制,也许你觉得它很有用。 您可以尝试逐字节创建PNG文件。调色板信息必须保存在 PLTE 块中,透明度值必须保存在 tRNS 块中。有关详细信息,请参阅PNG Specifications。 【参考方案1】:

我知道为什么您的 PNG 文件以高颜色保存。实际上,它们不是“保存为高颜色的 8 位图像”;问题是,从它们包含透明度的那一刻起,它们就被 .Net 框架加载为高颜色。图像本身非常好,只是框架把它们弄乱了。

这里发布了解决方法:

A: Loading an indexed color image file correctly

As wip said,不过,如果你想保留原始字节,调色板的实际更改应该使用块来完成,而不是通过 .Net 图形类,因为 .Net 重新编码将不可避免地改变字节。而且,有趣的是,我刚刚给出的调色板问题的修复已经包含了你需要的一半代码,即块读取代码。

编写代码是这样的:

/// <summary>
/// Writes a png data chunk.
/// </summary>
/// <param name="target">Target array to write into.</param>
/// <param name="offset">Offset in the array to write the data to.</param>
/// <param name="chunkName">4-character chunk name.</param>
/// <param name="chunkData">Data to write into the new chunk.</param>
/// <returns>The new offset after writing the new chunk. Always equal to the offset plus the length of chunk data plus 12.</returns>
private static Int32 WritePngChunk(Byte[] target, Int32 offset, String chunkName, Byte[] chunkData)

    if (offset + chunkData.Length + 12 > target.Length)
        throw new ArgumentException("Data does not fit in target array!", "chunkData");
    if (chunkName.Length != 4)
        throw new ArgumentException("Chunk must be 4 characters!", "chunkName");
    Byte[] chunkNamebytes = Encoding.ASCII.GetBytes(chunkName);
    if (chunkNamebytes.Length != 4)
        throw new ArgumentException("Chunk must be 4 bytes!", "chunkName");
    Int32 curLength;
    ArrayUtils.WriteIntToByteArray(target, offset, curLength = 4, false, (UInt32)chunkData.Length);
    offset += curLength;
    Int32 nameOffset = offset;
    Array.Copy(chunkNamebytes, 0, target, offset, curLength = 4);
    offset += curLength;
    Array.Copy(chunkData, 0, target, offset, curLength = chunkData.Length);
    offset += curLength;
    UInt32 crcval = Crc32.ComputeChecksum(target, nameOffset, chunkData.Length + 4);
    ArrayUtils.WriteIntToByteArray(target, offset, curLength = 4, false, crcval);
    offset += curLength;
    return offset;

我使用的Crc32.ComputeChecksum 函数是the Sanity Free Coding CRC implementation 的数组内改编。将其调整为给定数组内的可变起始和长度应该不难。

字节写入类ArrayUtils 是我制作的一个工具集,用于在具有指定字节顺序的数组中读取和写入值。它在this answer 末尾的 SO 上发布。

【讨论】:

以上是关于使用 BitmapSource、PngBitmapEncoder/Decoder 时,在 PNG 中保存 8 位索引数据会改变原始字节的主要内容,如果未能解决你的问题,请参考以下文章

在 LyncSDK 视频对话中将传出视频源设置为 BitmapSource?

BitmapSource

如何将BitmapSource转换为位图

为啥在 WPF 中将 BitmapSource 保存为 bmp、jpeg 和 png 时会得到完全不同的结果

把BitmapSource图片数据保存到文件

如何在不同线程的 C++/CLI 中将图像数据从 BitmapSource (WPF) 复制到 cv::Mat (OpenCV)?