颠倒黑白 (Inverse) ——利用 .NET Framework 实现图像反色效果小记

老白在开发WhiteNautilus Magnify (一个具有放大屏幕指定区域并反色显示该区域等功能的屏幕放大程序)时,为了较好地实现反色效果,特地跑到 Google 上用关键字“C# 反色”简单搜索了一下,发现位于搜索结果前几位的方法都大同小异[1][2]:大意是进行二重循环,对 Bitmap 的每个像素调用 GetPixel() 以获取其颜色值,然后对该像素颜色的 R, G 和 B 每个分量都用 255 减去原分量值,将得到的新值再用 SetPixel() 设置回去。老白照猫画虎,照葫芦画瓢,照着张飞画李逵……地照做了,然后遭遇了明显的性能问题。

可能也赶上程序有两点比较特殊:第一,放大镜有一种模式是窗体的位置始终跟踪鼠标指针(用了 Timer  以定时更新窗体位置);第二,窗体是使用 Region 裁切过的异形窗体。如果再加上那个二重循环,其整体性能简直让人无法忍受(咱这奔腾 M 1.5G 按说也不是闹着玩的)……没办法,谁让老白已经滥用了很多系统资源呢,只好自己再想办法……

虽然接触 .NET 有段时间了,也曾经参与过使用 ASP.NET 技术的动态网站的开发、测试,但是对整个 Class Library 还是没有完整的概念,GDI+ 就更甭提了。但是老白总有一种感觉,Class Library 应该具备处理类似问题的能力。浏览了 MSDN 中几个相关类的文档后,偶然发现了 Recoloring Images 这一专题。听起来挺对路子,那就赶紧看看吧~

果然,其中的几个小专题分别介绍了利用“颜色变换矩阵”(Color Matrix,请原谅老白擅自增加了“变换”二字)进行颜色的平移、缩放、扭曲等操作(这就是抽象的力量:将颜色(R, G, B 和 A)、三维空间中的点(X, Y 和 Z)和方向等都抽象成带有多个分量的向量,对其的操作比如平移、旋转和缩放,都是将其乘以一个特定的矩阵就完事儿)。老白想,既然 .NET 提供了这种重新着色的方式,估计是做了优化的(恨自己当年没好好学 Win32 API  编程,只听说过 BitBlt 云云),可以尝试一下,于是开始琢磨怎么设计这个颜色变换矩阵。

不知正在阅读本文的您是否学习过线性代数,学过的是否还记得矩阵相乘?这个……相关知识可以参考教科书或者在网上进行搜索,恕不赘述。但为了避免您频繁切换于本文和参考文献之间,老白还是准备在这里稍微引述一下相关内容,有时间和兴趣的读者亦可直接参看上文提及的 MSDN 中的专题  Recoloring Images,其中从颜色的向量表示法到颜色变换矩阵等,对所需的基础知识都有比较系统的介绍。

我们知道,一种颜色可以使用 R, G, B 和 A 一组共四个数来表示,其中每个数的取值范围可能是 0~255 或 0.0~1.0(至于 Web 开发习惯使用八位十六进制等形式表示,则不在讨论之列)。为了参与矩阵操作,还需增加一个所谓“齐次分量”,在下文中我们将可以看到齐次分量的重要作用。这样的话,不透明的纯红色就可以使用 [1.0, 0.0, 0.0, 1.0, 1.0] 这个向量来表示(选择 0.0~1.0 这种表示法是为了方便矩阵运算),其中的分量从左到右依次是红色、绿色、蓝色、Alpha(不透明度)和齐次分量。

参照上文提到的反色计算方法,我们需要对 R, G 和 B 每个分量进行“255 - 原值 = 新值”或“1.0 - 原值 = 新值”,而这种运算如何通过乘以一个矩阵来完成呢?老白就不卖关子了,直接拿最后使用的颜色变换矩阵举例了:

 
 
| 1 0 0 1 1 |   X
 
 
| -1  0  0 0 0 |
|  0 -1  0 0 0 |
|  0  0 -1 0 0 |
|  0  0  0 1 0 |
|  1  1  1 0 1 |
 
 
=   | 0 1 1 1 1 |
 
 

根据矩阵乘法的规则,将左矩阵的第 i 行的每个元素与右矩阵第 j 列的相应元素相乘,再把这些乘积相加,即得到结果矩阵第 i 行的第 j 个元素。所以,运算结果的前两个分量是这样计算得到的:

1x(-1)+0x0+0x0+1x0+1x1 = 0
1x0+0x(-1)+0x0+1x0+1x1 = 1

其他依此类推。这样我们就通过矩阵乘法运算完成了三个颜色分量的"-1x原值+1 = 新值"的运算,和上文所述一致。

最后的程序代码类似以下这样:

//  准备颜色变换矩阵的元素
float [][] colorMatrixElements  =   {
   
new float[] {-10000},
   
new float[] {0-1000},
  
new float[] {00-100},
   
new float[] {00010},
   
new float[] {11101}
    }
;

//  为 ImageAttributes 设置颜色变换矩阵
ColorMatrix colorMatrix  =   new  ColorMatrix(colorMatrixElements);
ImageAttributes imageAttributes 
=   new  ImageAttributes();
imageAttributes.SetColorMatrix(colorMatrix,
     ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

//  将 ImageAttributes 应用于绘制
graphics.DrawImage(image,
    
new  Rectangle( 0 0 , image.Width, image.Height),
    
0 0 , image.Width, image.Height,
     GraphicsUnit.Pixel, imageAttributes);

也就是使用带有 ImageAttributes 类型参数的 DrawImage() 把图像在原地重绘一遍,注意传入的 ImageAttributes 的实例已经初始化为使用我们提供的颜色变换矩阵。

改用这种方法后,老白用肉眼就可以看到性能改善,总算达到了目的。希望本文能对遇到类似问题的朋友有所助益。

再说几句:
1.如果颜色变换矩阵运算结果超出了 0.0~1.0 的范围,.NET 会自动将其“和谐”到合法范围,即凡是大于 1.0 的都作为 1.0 处理,下限类似。这种行为是不是叫做“单位化”来着,normalize?
2.理解原理以后,大家可以自己设计颜色变换矩阵,从而轻松实现很多图像效果。老白怀疑已经有很多现成的矩阵,只是还没找到……
3.老白在校时,虽说也知道“计算机=数学+物理”,无奈数学系列学得都不是很好,而且认为所学的东西只有物理、电路这些后续课程能直接用上,实际应用时要不就根本用不上,要不就是用现成的。现在看来,即使有现成的类库,也还需要运用自己的数学知识和聪明智慧,从而高效地完成任务。

参考文章:
[1] C# 通过光栅运算实现反色和模糊效果
[2] 使用 GDI+ 制作反色图片

老白后记:原文于2007年8月5日发表于老白的 Google Group,链接地址如下:
http://groups.google.com/group/whitenautilus/web/inverse?hl=zh-CN
本文在原文基础上进行了少量修改。

你可能感兴趣的:(技术技巧,(Technology,&,Technique))