65.9K
CodeProject 正在变化。 阅读更多。
Home

GDI+ 深色处理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (6投票s)

2012年10月23日

CPOL

5分钟阅读

viewsIcon

24499

如果您在 WinForms 和 GDI+ 中投入了大量代码,并且对 GDI+ 缺乏深度颜色(> 24 位/像素)支持感到沮丧,那么请继续阅读有关此绕道方案的原理。

引言

如果您在 WinForms 和 GDI+ 中投入了大量代码,并且对 GDI+ 缺乏深度颜色(> 24 位/像素)支持感到沮丧,那么请继续阅读有关此绕道方案的原理,其中包含一些代码片段,但没有完全编码的解决方案(它不完全属于我,不能随意赠送!)。我假设读者对 GDI+ 和 GDI+ 图像处理有一定的背景了解。

背景

GDI+ 是使用 System.Drawing 命名空间在 WinForm 应用程序中进行绘图和成像的基础技术。它相当老旧,似乎不再更新。它已被 WPF 中的 System.Windows.Media 命名空间类所取代,而这些类与旧的 GDI+ 类不兼容。换句话说,如果您像我一样,编写了大量使用 GDI+ 位图的图像处理例程,那么您将需要重写它们以使用 WPF 媒体图像(WriteableBitmap)。此外,如果您的 UI 层在 Winforms 中,您将不得不调整它们来显示 WPF 位图。总之,这是一项艰巨的工作,而且我完全不确定它是否值得,因为 .NET 中正在发生很多变化(Metro?)。

现在,GDI+ 位图乍一看支持 48 位/像素 (bpp) 图像(有一个 Format48bppRgb 像素格式)。这是具有欺骗性的,因为

  • GDI+ 无法打开和保存 48 bpp 图像。
  • 它实际上不是 48 bpp,而是 39 bpp,伽马值为 1。

没错,48 bpp 图像格式使用线性光编码,这意味着对于每种颜色通道 13 位,我们实际上并没有比使用伽马值为 2.2 的标准 24 bpp 图像格式获得任何优势,一点也没有。所以,如果您像我一样,需要深度颜色支持以获得更高的精度(或更大的动态范围),那么您就运气不佳了!请继续阅读有关此绕道方案的内容,该方案将允许加载和保存更高像素深度的图像,并能正确显示它们。

解决方案

首先要解决的问题是图像的加载和保存。WPF 支持深度颜色,因此我通过编写 GDI+ 位图和 WPF 位图之间的转换例程,仅使用 WPF 来创建 GDI+ 位图。这实际上相当简单,只是复制 GDI+ 位图缓冲区和 WPF 位图缓冲区之间的内存(dstData 是 GDI+ 位图的 BitmapData,source 是 WPF WriteableBitmap)。

[DllImport("kernel32.dll", EntryPoint = "CopyMemory", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
    internal static extern void CopyMemory(void* PtrOut, void* PtrIn, int byteCount); 
CopyMemory((void*)dstData.Scan0.ToPointer(), (void*)source.BackBuffer.ToPointer(), nrBytes);

现在,我知道这并不适用于 WPF 中的所有格式,并且在颜色分量顺序不同(BGR 与 RGB)时会导致问题,但它似乎适用于 1、8 和 24 bpp BGR 图像,因为 WPF 中的这些也是 BGR。尝试以这种方式加载 48 bpp 图像时,显示效果如下:

为什么会这样?嗯,如前所述,我们的 GDI+ 只使用 39 bpp,所以我们需要将 WPF 图像数据映射到该范围。

[System.Security.SecuritySafeCriticalAttribute]
    private static unsafe void ConvertData(WriteableBitmap source, BitmapData dstData, ushort[] LUT)
      {
      //Must be done in main thread, as WPF image has thread affinity!
      ushort* srcPixel0 = (ushort*)source.BackBuffer.ToPointer();
      ushort* dstPixel0 = (ushort*)dstData.Scan0.ToPointer();
 
      int dstStride = dstData.Stride / 2, srcStride = source.BackBufferStride / 2, width = source.PixelWidth;
      unsafe
        {
        Action<int> processRow = y =>

        {
          ushort* src = srcPixel0 + y * srcStride;
          ushort* dst = dstPixel0 + y * dstStride;
          for (int x = 0; x < width; x++)
            {
            for (int i = 0; i < 3; i++) *(dst + 2 - i) = LUT[*(src + i)];
            src += 3; dst += 3;
            }
        };
        System.Threading.Tasks.Parallel.For(0, source.PixelHeight, processRow);
        } 
      }

LUT(查找表)只是一个快速有效的方法,可以将 [0 - 2^16] 映射到 [0 - 2^13],并且实现起来很简单。线性(按比例)进行此操作可以保持与原始数据相同的伽马校正。

for (int i = 0; i <= ushort.MaxValue; i++) LUT[i] = (ushort)(i >> 3);

请注意,在内层循环中,RGB 被重新排序为 BGR,因为 GDI+ 中的 48 bpp 是 RGB!但是,在显示此图像时,我们又遇到了奇怪的情况:

图像看起来完全被冲淡了。事实证明,默认情况下,GDI+ 将 48 bpp 图像解释为伽马为 1(即线性数据)。我们不想存储线性数据,因为这与 24 bpp 图像相比没有任何改进(请参阅下一段,其中包含一些关于为什么会这样做的技术背景)。幸运的是,在 GDI+ 中显示图像时,可以通过包含 ImageAttributes 来设置或覆盖应用的伽马校正。

Dim attr As New Imaging.ImageAttributes
attr.SetGamma(2.2)
Me.Graphics.DrawImage(.Bitmap, rect, 0, 0, rect.Width, rect.Height, GraphicsUnit.Pixel, attr, Nothing)

关于线性数据和伽马校正的技术背景:基本上,使用线性数据时,像素值与其下一个像素值之间的感知颜色变化在所有可能的像素值上都非常不均匀,而且通常在暗颜色处变化更大。因此,为了避免在那些暗区域出现任何条带,需要更多的位来提供颜色之间更平滑的过渡。由于自然界的一个巧合,通常应用于线性颜色三刺激值(例如 RGB)以补偿阴极射线管显示设备非线性行为的非线性伽马校正是与我们的视觉系统处理光的机制相关的。结果是,对于伽马校正的数据,像素值与其下一个像素值之间的感知颜色差异或多或少是恒定的,这意味着可以使用更少的位来编码像素值而不会产生明显的条带。这就是为什么即使使用我们现代的基于线性 TFT 的显示设备,仍然使用伽马校正,因为它降低了带宽要求。

在我们的 Winform 应用程序中显示 48 bpp 图像,最终获得了期望的结果。

显然,为了保存 48 bpp 图像,我们需要从 GDI+ 位图中创建一个 WPF 图像,然后保存该图像。转换同样可以使用 LUT,这次是将 [0 - 2^13] 映射到 [0 - 2^16]。

    /// <summary>
    /// Transform data from a 48 bpp GDI bitmap to a WPF bitmap 
    /// </summary>

    /// <param name="sourceData"></param>
    /// <param name="destination"></param>
    /// <param name="LUT"></param>

    [System.Security.SecuritySafeCriticalAttribute]
    private static unsafe void ConvertData(BitmapData sourceData, WriteableBitmap destination, ushort[] LUT)
      { 
      //Must be started in main thread, as WPF image seems to have thread affinity!
      ushort* srcPixel0 = (ushort*)sourceData.Scan0.ToPointer();
      ushort* dstPixel0 = (ushort*)destination.BackBuffer.ToPointer();
      int dstStride = destination.BackBufferStride / 2, srcStride = sourceData.Stride / 2, width = sourceData.Width;
      unsafe
        {
        Action<int> processRow = y =>
           {
             ushort* src = srcPixel0 + y * srcStride;
             ushort* dst = dstPixel0 + y * dstStride;
             for (int x = 0; x < width; x++)
               {
               for (int i = 0; i < 3; i++) *(dst + 2 - i) = LUT[*(src + i)];
               src += 3; dst += 3;
               }
           };
 
        System.Threading.Tasks.Parallel.For(0, sourceData.Height, processRow);
        }
      }

现在,48 bpp 图像的典型图像处理例程将使用 ushort 指针而不是通常的 byte 指针,这与之前的代码片段类似。

结论

我知道这只是一个绕道方案,最终,将所有图像处理例程编写在某些本地内部图像格式(例如,多维双精度数组或锯齿状数组)上可能更有效,然后仅提供到 GDI+、WPF 和微软接下来推出的任何内容的输入和输出转换例程。我已经为此实现了一个基本版本,它的速度慢了大约 2 倍,并且显然消耗了更多的内存。与此格式的转换非常快,并且具有额外的优点,即在连续执行多个图像处理操作时不会丢失精度,因为我们不需要在每次操作后将其存储在整数位图格式中。在完全实现此功能之前,此绕道方案对我来说效果很好……

© . All rights reserved.