对 ASP.NET 图像的颜色量化(Quantization)进行优化(From MS)

对 ASP.NET 图像的颜色量化(Quantization)进行优化


Morgan Skinner
Microsoft Developer Services
2003年5月

摘要:介绍如何动态量化(重新着色)为 ASP.NET 页生成的图像,从而克服 .NET Framework 1.0 和 1.1 版中的 GDI+ 局限性。本文介绍了两种降低图像颜色深度的方法。

适用于:
   Microsoft® Framework 1.0 和 1.1 版
   Microsoft® Visual C#®
   Microsoft® ASP.NET

下载 DotNET_Color_Quantization_Code.exe 示例文件(英文)。

目录

简介

大多数 Web 站点都会包含各种格式的图形,例如 MSDN Web 站点的横幅标题和用于新闻标题列表的缩略图图像。所有这些图像都是静态的 - 它们由 Web 组的一个成员生成,被重新着色以满足 Web 站点的要求,然后存储到磁盘中以备适当时使用。

使用 ASP.NET,还可以在处理当前 Web 请求的过程中创建动态图像。此功能可以使站点个性十足,或者生成符合一定外观风格的图像,而不需要 Web 设计师的服务。

图像的生成相对比较简单:创建一个用于绘图的表面,显示相应的图像,然后将其保存到要返回给用户的 ASP.NET 响应流中。

这种方案唯一的问题就是生成的图像质量。默认情况下,GDI+ 会利用 Web 安全调色板,将位图转换为适用于 Web 页的图像(例如 GIF 或 JPEG)。(有关详细信息,请参阅本文最后的更多信息。)因此生成的图像质量会比较差,而且还可能包含由于使用减色算法而产生的各种杂色。这种利用 Web 安全调色板进行的转换发生在图像转换为输出类型(例如 GIF 或 JPEG)之前,因此,即使输出类型能够支持多种颜色,生成图像时仍然会使用 Web 安全调色板中的颜色。

除了生成的图像质量以外,为图像重新着色还有一个原因就是速度 - 通常从 Web 站点下载每像素 1 字节的图像比下载每像素 4 字节的图像速度要快,而且并不是每个人都有 ADSL。此外,某些浏览器(例如 WebTV)的颜色空间有一定的限制。因此,为了能向客户提供高质量的图像,重新着色是必要的。

为图像重新着色以指定颜色数较少的调色板的过程称为“量化(Quantization)”。

问题概述

为了查看默认 GDI+ 图像显示的结果,我将提供一个简单的图像生成示例,然后在请求此图像的 Web 页上显示输出的结果。

假设您在为一家网上银行工作。在信用卡注册过程中,客户可以从预定义的图像中进行选择来个性化他们的信用卡。您可能已经在注册过程中获得了客户姓名,因此最好能够在图像上浮凸显示客户姓名,从而标识信用卡。

图 1:(虚构)MSDN 信用卡的背景位图

向用户显示图像时,将先加载背景位图(MSDN 信用卡),然后再在图像上显示一些文字来个性化图像。以下代码显示了如何从程序集清单中加载图像,如何在上面浮凸显示客户姓名,然后又如何从 ASPX 页返回此图像。

// 获得背景图像资源数据流
Assembly assem = Assembly.GetExecutingAssembly ( ) ;

using ( Stream   s = assem.GetManifestResourceStream 
                           ( "TestApp.MSDNCard.gif" ) )
{
   // 并将资源加载到位图中
   Bitmap   background = Bitmap.FromStream ( s ) as Bitmap ;

   Bitmap copy = new Bitmap ( background ) ;

   // 现在从位图构造图形对象
   // 以便在上面显示
   using ( Graphics g = Graphics.FromImage ( copy ) )
   {
      // 获取信用卡式的字体
      Font textFont = new Font ( "Lucida Console" , 10 , FontStyle.Bold ) ;

      // 显示持卡人的姓名
      g.DrawString ( cardholder , textFont , Brushes.Silver , 12 , 130 ) ;
      g.DrawString ( cardholder , textFont , Brushes.White , 13 , 131 ) ;

      // 然后是有效日期
      StringFormat   fmt = new StringFormat ( ) ;
      fmt.Alignment = StringAlignment.Far ;

      g.DrawString ( "June 04" , textFont , Brushes.Silver , 
        new RectangleF ( 186 , 130 , 100 , 20 ) , fmt ) ;
      g.DrawString ( "June 04" , textFont , Brushes.White , 
        new RectangleF ( 187 , 131 , 100 , 20 ) , fmt ) ;
   }

   // 返回图像
   Response.ContentType = "image/gif" ;

copy.Save ( Response.OutputStream , ImageFormat.Gif ) ;
}

此示例中的图像包含在当前的程序集中。要自己完成此操作,请将某个图像(或其他资源)添加到您的项目中,然后将 Build Action(生成操作)设置为 Embedded Resource(嵌入资源)。在 Visual Studio .NET 中,您可以从 Property(属性)窗口访问任何文件类型的 Build Action(生成操作)属性。

然后在图像上绘制一些文字。至此,我已经通过先绘制银色文字,然后在 X 轴和 Y 轴上偏移一个像素,再绘制白色文字的过程,创建了粗略的浮凸外观。此过程生成的是假的浮凸外观,但用于本示例已经足够了。

当此图像返回到 ASP 页(或保存到磁盘中)时,将使用 Web 安全调色板来减少输出的颜色。无论采用何种输出类型(GIF、JPEG 等),都将得到以下图像。

图 2:使用默认调色板返回经过抖色处理的图像

正如此图的标注所描述的,生成的图像非常粗糙。这是由于对图像进行了抖色处理,将图像的颜色减到了要求的 256 色;再者,生成的图像使用的是 Web 安全调色板,而不是 256 色的自定义调色板。

我们需要一种方法将图像的颜色深度降到 256 色(或更少),并且生成包含专用于 GIF 图像的调色板信息的 GIF 图像。此过程通常称为“量化”。

量化概述

量化图像的过程很容易描述。为源图像中的每个像素,查找或计算用于定义目标图像中该像素的颜色的值。当颜色深度降到 256 色时,通常意味着生成的颜色是调色板的一个索引。

例如,假设一个图像包含 5000 种颜色,而我们希望将它减少为 256 色的 GIF 图像。显然,得到的图像将丢失一些颜色。但是一种好的量化算法会最大限度地减少颜色的丢失,从而使生成的图像可以接受。

第一步,您可以从源图像中获得 256 种颜色,然后将这些颜色用作整个图像的调色板。所有后续颜色都将被映射到最初的 256 种颜色。由于这种方法不考虑图像中颜色的出现频率,因此生成的图像质量常常较差。如果第 257 种颜色在图像中的使用率达到 50%,最好确保该颜色在目标图像中保持不变。

另一方面,我们可以创建带有 32 KB 槽的数组,并在每个槽中生成像素的累计数量。然后我们需要确定哪些像素将被提取到输出调色板中,哪些像素将被转换成调色板颜色。我们可以根据颜色的使用频率由高到低依次选出各种颜色,直到装满调色板。

在每一种情况下,算法都包含两个主要部分:第一部分是遍历源数据,第二部分是量化像素并生成最终图像和颜色调色板。了解了这一点,我们就可以创建算法的第一部分 - 遍历图像(每次一个像素),然后适当地插入不同的量化器。

关于位图的一点常识

为了得到我们的算法,我们需要稍微了解一下位图的内部(内存)表示法。在本示例中,我们仅考虑 8 BPP(位/像素)或 32 BPP 的图像。在 8 BPP 图像中,每个像素由一个字节标识,所以 16×16 像素的图像一共需要 256 个字节。相同大小的 32 BPP 图像则需要用 1024 个字节来标识,因为每个像素需要 4 个字节。二者都忽略了与位图相关的系统开销,例如位图的维数。

在计算机的黑色时代(颜色很有限),显示适配器可能只有 256 种颜色可供使用(1983 年我的第二台计算机就是这样),因此一个像素的颜色是通过单个字节来标识的。然而在今天,每个人甚至连宠物狗的适配器都拥有 32 KB 或更多种颜色,因此现在一个字节用来表示颜色调色板的一个索引。因此,写入 8 BPP 图像的过程也相应地分为两个阶段 - 计算源位图中每个像素的调色板索引,以及设置调色板项。

图 3:32 BPP 图像与 8 BPP 图像

32 BPP 图像由红、绿、蓝颜色分量各 8 位,和 8 位“alpha”信道信息组成。这个“alpha”分量用于确定像素的透明度,其中,值 255 表示完全不透明,值 0 表示完全透明。在本示例的余下部分,我们不再涉及此分量。但是您可以设置预定义值,使任何“alpha”分量小于预定义值的颜色在生成的 GIF 中显示为透明的颜色。

由于我们将使用指针来加快进程,因此遍历源图像中的字节需要一些不安全的代码。我们可以使用 Bitmap 类中的 GetPixel 函数,但是此函数在遍历图像中的所有像素时速度不够快。

以下代码将遍历源图像中的所有像素,并在示例代码中用作第一次遍历数据的代码。

BitmapData   sourceData = null ;

try
{
   // 将源数据锁定到内存中
   sourceData = bmp.LockBits ( new Rectangle ( 0 , 0 , width , height ) , 
                               ImageLockMode.ReadOnly , 
                               PixelFormat.Format32bppArgb ) ;

   // 定义源数据指针。源行是一个字节,
   // 它可以使步长值的递增更容易(由于使用字节为单位)。
   byte*   pSourceRow = (byte*)sourceData.Scan0.ToPointer ( ) ;
   Int32*   pSourcePixel ;

   // 遍历每一行
   for ( int row = 0 ; row < height ; row++ )
   {
      // 将源像素设置为这一行的第一个像素
      pSourcePixel = (Int32*) pSourceRow ;

      // 并遍历每一列
      for ( int col = 0 ; col < width ; col++ , pSourcePixel++ )
      {
         // 用 *pSourcePixel 执行某些操作
      }

      // 按步长递增源行
      pSourceRow += sourceData.Stride ;
   }
}
finally
{
   // 确保数据未被锁定
   bmp.UnlockBits ( sourceData ) ;
}

遍历图像位非常简单 - 将指针设置到内存中图像的开始处,然后递增到下一个像素。每遍历完一行后,就按照步长值递增行指针,然后再次遍历列。

位图的步长是每行占用的字节数,可以与图像中的像素数相同,也可以不同。例如,一个 13 位宽的 8 BPP 图像的步长可以是 16,因为对于 32 位的计算机,这是个很理想的循环数。

生成输出位图

用于生成输出位图的代码与上面显示的用于遍历输入位以生成颜色调色板的代码非常相似。

在本示例中,我们现在开始遍历源位图和目标位图中的每个像素,并将源像素经过计算的调色板索引分配给目标像素。完成后,我们就应该得到一个可以显示给用户的完全量化的图像。以下代码片段是本文示例所使用的算法的简化版本。

for ( int y = 0 ; y < height ; y++ )   for ( int x = 0 ; x < width ; x++ )
      destinationPixel[x,y] = Quantize ( sourcePixel[x,y] ) ;

实际使用的算法并不象上面描述的这么简单,因为与将位图作为二维数组进行处理相比,遍历位图还有更快捷的方法。

转换完所有像素后,还有最后一件事要做 - 我们需要将调色板与新生成的图像关联,以使输出图像能够按照已完成的量化正确显示。

基于调色板的量化

我将要介绍的第一个示例基于已知的调色板。许多 Web 站点都含有一组自定义颜色,用于显示商标,例如 http://www.gotdotnet.com/(英文)所使用的蓝色。这里我们要做的是生成一个图像,然后使用通过某种方式生成的调色板对该图像重新着色。在本示例中,我已经使用图像编辑器为虚构的 MSDN 信用卡构造了调色板。

基于调色板的算法的工作原理是在输出调色板中为源图像的每个像素选择最接近的颜色。

此方法的好处是容易理解。但是,对于受输入调色板限制的情况,此方法不能自适应。如果图像与调色板的颜色相差很大,则图像质量将受到影响。

为将给定像素从源颜色转换为目标调色板索引,我编写了一个方法,通过遍历调色板来查找 RGB 值最接近源颜色的颜色。该算法的具体编码如下。

int   leastDistance = int.MaxValue ;
int red = pixel->Red ;
int green = pixel->Green;
int blue = pixel->Blue;

// 遍历整个调色板,查找最接近的颜色匹配
for ( int index = 0 ; index < _colors.Length ; index++ )
{
   // 从调色板中查找颜色
   Color   paletteColor = _colors[index];
   
   // 计算源颜色与调色板颜色之间的差距
   int   redDistance = paletteColor.R - red ;
   int   greenDistance = paletteColor.G - green ;
   int   blueDistance = paletteColor.B - blue ;

   int      distance = ( redDistance * redDistance ) + 
                       ( greenDistance * greenDistance ) + 
                       ( blueDistance * blueDistance ) ;

   // 如果某种颜色比目前找到的所有颜色都接近源颜色,则使用该颜色
   if ( distance < leastDistance )
   {
      colorIndex = (byte)index ;
      leastDistance = distance ;

      // 如果是完全匹配的颜色,则停止循环
      if ( 0 == distance )
         break ;
   }
}

此处使用了简单的最小平方算法来查找最接近的颜色匹配,并且使用散列表来存储找到的颜色,因此遍历调色板时只需要查找尚未量化的颜色。对于含有大量颜色的图像,这种方法的内存开销会比较大。因此如果没有缓存,该算法会比较慢。

基于八叉树的量化

基于调色板的算法相对较快,但缺点是它不是自适应算法。与其相比,八叉树可以处理任何图像,而无论图像中有多少种不同的颜色。通过八叉树将图像的大量颜色转换成较为合理的颜色数时,生成的结果会比较理想,只是图像的质量稍有降低。不足之处在于,该算法的计算开销更大,在量化同一图像时,该算法通常比调色板算法花费的时间更长。

名称“八叉树”来源于用来表示图像颜色的数据结构。它由许多节点组成,每个节点最多拥有 8 个子节点。

假设有一个要量化的颜色值 (#6475D6)。该颜色被分为独立的红色、绿色和蓝色分量,然后后续位按照红色、绿色和蓝色值进行移位,生成介于 0 到 7 之间的数值。这个值将决定在创建树时,以及在搜索最接近的颜色值时,从当前节点遍历到哪个叶。

红色 (0x64)01100100
绿色 (0x75)01110101
蓝色 (0xD6)11010110
结果47360742

上面显示的结果是每个分量颜色值的位组合,分别对 RGB 颜色中的每一位进行了计算,如下所示。

result = red | green<<1 | blue<<2 ;

按从最重要到最不重要的顺序遍历位,所以在我的示例中,先移出最重要的位(第 7 位),然后是次重要的位(第 6 位),依此类推。对于上面的示例,树中遍历的节点如下所示。

图 4:遍历八叉树以查找颜色 #6475D6

正如前面所述,颜色被插入到八叉树中时,会遍历节点。当找到叶节点时,将在该节点中递增此颜色的像素计数,红色、绿色和蓝色分量将分别被添加到该节点中的 R、G 和 B 值中。这些数值用于根据每个颜色值(按具有该颜色的像素数划分)生成一种平均颜色。

无需任何整理,您的树最终就会拥有与输入图像中的颜色数一样多的叶节点。为了避免使树无限增长,可以预设八叉树的叶数。在量化完整个图像后,八叉树的叶将形成颜色调色板。

以前在 MSDN 上开展过有关八叉树的讨论,所以我重复利用了 Jeff Prosise 的 Wicked Code(英文)一文中的代码,该文章发表在 Microsoft Systems Journal (MSJ) 1997 年 10 月号上。在本示例中,我已经将 Jeff 的代码从 Visual C++ 转换为 C#,同时还做了几处修改。

对 Jeff 的八叉树算法进行的主要修改是提高了量化循环的性能。在我的版本中,对每个量化调用的结果都进行了缓存,如果有大量彼此相邻的像素具有相同的颜色,这样做可以加快进程。对每个循环的遍历,我都检查当前像素的颜色,如果它与上一个量化的像素匹配,则可以绕过代码,而只返回上一结果。

示例 Web 站点

本文附带的代码包括基于调色板和基于八叉树的两种量化算法。量化的结果将显示在原始图像和以 GDI+ 进行了量化的图像(看起来非常粗糙)旁边。(请注意在下面图 5 的比较中我省略了原始图像,但是您可以在代码示例中查看该图像。)

为了生成图像,我为每个图像类型编写了一个简单的 ASPX 页,其中包含的代码与下面显示的代码类似。

public void RenderImage ( )
{
   using ( Bitmap image = Generator.GenerateImage ( "Octree Quantized" ) )
   {
      OctreeQuantizer   quantizer = new OctreeQuantizer ( 255 , 8 ) ;

      using ( Bitmap quantized = quantizer.Quantize ( image ) )
      {
         Response.ContentType = "image/gif" ;

         quantized.Save ( Response.OutputStream , ImageFormat.Gif ) ;
      }
   }
}

这会使用八叉树量化器生成信用卡的量化图像,并将响应类型设置为 image/gif。它作为 img 标记包含在示例 Web 站点的页面中:<img src="OctreeQuantization.aspx">。每个 ASPX 页只包含一个对以上 RenderImage 函数的调用。

量化进程的结果如下所示。

量化类型图像图像大小(以字节为单位)

无(默认为 GDI+)

21342

基于调色板

16748

基于八叉树

16744

图 5:量化结果比较

调色板和八叉树的结果比使用 Web 安全调色板的默认 GDI+ 显示小 20%,图像质量也大大提高。基于调色板和基于八叉树的图像不一样,因为图像包含略有不同的文字。您的结果根据图像的大小可能会有所不同,但是能够生成更小且质量更高的图像还是值得的。

小结

使用 ASP.NET,可以生成图像,然后将其显示在 Web 站点上,通过这种方式为单个用户或一组用户个性化站点。这样做的优点有很多方面。例如,您可能需要根据当前用户的语言设置来生成按钮的名称。为了避免生成大量实质相同的图像(文字除外),可以生成一个通用的按钮图像,然后在运行时显示文字。

本文介绍了两种通过 .NET 进行量化的方法,但是还有许多其他算法,您不妨自己去尝试一下。代码编写时考虑了扩展性,因此可以通过从 Quantizer 类派生并重写 QuantizePixelGetPalette 以及 InitialQuantizePixel(可选)方法来插入您自己的量化器。代码提供了有关如何插入您自己的算法的示例。

更多信息

关于作者

Morgan 是 Microsoft 在英国工作的应用程序开发顾问,专攻 Visual C#、控件、WinForms 和 ASP.NET。自从 2000 年发布 PDC 以来,他就从事 .NET 工作,并且非常喜欢 .NET,因此加盟了该公司。他的主页是 http://www.morganskinner.com/(英文),在这儿您可以找到他写的其他文章的链接。在有限的闲暇时间里,他喜欢在自家的花园中除除草,或者享受几块风味独特的菜肉烘饼。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页