您当前位置: 圣才学习网首页 >> IT类 >> .NET程序设计

GDI+高频问题思考(4)

扫码手机阅读
用圣才电子书APP或微信扫一扫,在手机上阅读本文,也可分享给你的朋友。
评论(0
   
来源:网络 作者:未知
 
  5.读图是快了,处理怎么还是慢?
 
  GDI+的Bitmap类提供了两个罪恶的函数GetPixel SetPixel,用来获取某个像素点的颜色值。这个2个函数如果只调用一次两次也就罢了,万一我想把整张图片加红一点,用下面的代码,我估计你等到黄花菜都凉了,还没有算完呢。看看下面的代码是怎么写的。
 
  1 FileStream fs = new FileStream(image, FileMode.Open, FileAccess.Read);
 
  2 Image img = Image.FromStream(fs, false, false);
 
  3 Bitmap bmp = new Bitmap(img);
 
  4 img.Dispose();
 
  5 fs.Close();
 
  6
 
  7 for (int j = 0; j < bmp.Height; j++)
 
  8 {
 
  9      for (int i = 0; i < bmp.Width; i++)
 
  10      {
 
  11           Color color = bmp.GetPixel(i, j);
 
  12           color = Color.FromArgb(color.R + 20, color.G, color.B);
 
  13           bmp.SetPixel(i, j, color);
 
  14      }
 
  15 }
 
  代码逻辑很清楚,第1到第5行,写得很好,用了我们在前几节里面的方法,读图速度飞快且不锁文件。当然如果不用覆盖原始文件,不用复制都可以,速度就更快了。接下来我们对图像做一个循环,一行一行更新图像的数据。殊不知GetPixelSetPixelGDI里面耗费最大的函数之一,此外bmp.Heightbmp.Width也是慢得够呛,如果处理一张500M像素的照片,您可以去喝杯茶,睡一觉再回来了。
 
  Bitmap有个方法叫LockBits,就是把图像的内存区域根据格式锁定,拿到那块内存的首地址。这样就可以直接改写这段内存了。这个方法的设计是挺好,可惜都是C++作为源泉来的, .NET Framework里面根本就不推荐用指针,VB里面根本也就没有指针。最后设计了一个鸡肋IntPtr,将就掉了这个问题。C#其实还好,可以用unsafe code,也算是间接用了指针, VB.NET就痛苦了,需要用Marshal.Copy把内容Copy到一个byte数组里面,然后处理完了再 Copy回去。所以结论就是,要用GDI+做图像处理,最好别用VB.NET,否则内存翻倍。
 
  让我们来看看快速的写法,注意在编译的时候加上unsafe开关,允许C#使用指针:
 
  1 FileStream fs = new FileStream(image, FileMode.Open, FileAccess.Read);
 
  2 Image img = Image.FromStream(fs, false, false);
 
  3 Bitmap bmp = new Bitmap(img);
 
  4 img.Dispose();
 
  5 fs.Close();
 
  6
 
  7 int width = bmp.Width;
 
  8 int height = bmp.Height;
 
  9 BitmapData bmData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
 
  10 byte* p = (byte*)bmData.Scan0;
 
  11 int offset = bmData.Stride - width * 3; //only correct when PixelFormat is Format24bppRgb
 
  12
 
  13 for (int j = 0; j < height; j++)
 
  14 {
 
  15      for (int i = 0; i < width; i++)
 
  16      {
 
  17           p[2] += 20; //should check boundary
 
  18           p += 3;
 
  19      }
 
  20      p += offset;
 
  21 }
 
  22
 
  23 bmp.UnlockBits(bmData);
 
  24 bmp.Dispose();
 
  1-5行一样的,不多说了。第78行保存一个临时变量,不要每次都调用,bmp.Width bmp.Height
 
  第9行是把图像内容锁定到系统内存。这个函数有2个重载,第二种比较复杂,是把用户内存中的内容锁定。这个以后再说。第一种,也就是我们现在使用的这种,是把图像的内容根据一定的格式放到内存里面,这里我们使用的是Format24bppRgb,也就是24位色。在这种格式3个字节表示一种颜色,也就是我们通常所知道的RGB所以每个字节表示颜色的一个分量。
 
  第10行用了一个指针,指向这段内存的首地址。这样我们可以直接来修改图像的内存信息。因为每个颜色分量都是一个字节,所以用byte的指针,如果是Format48bppRgb,每个分量用2个字节,那就该用Short指针了。
 
  第11行需要多说两句,Stride是指图像每一行需要占用的字节数。根据BMP格式的标准, Stride一定要是4的倍数。据个例子,一幅1024*76824bppRgb的图像,每行有效的像素信息应该是1024*3 3072。因为已经是4的倍数,所以Stride就是3072。那么如果这幅图像是 35*30,那么一行的有效像素信息是105,但是105不是4的倍数,所以填充空字节,Stride该是108。这一行计算出来的offset就是3。这里再留个问题,如果是16色图,也就是4位色,一幅50*50的图像,Stride又应该是多少呢?
 
  第13-21行就是循环的处理。其中第17行需要注意以下。这句话其实是错的,因为p byte,没有符号,最大值是25520可能会溢出。如果你不希望它溢出,那么可以改成行1 ,如果希望溢出就是最大值,那么可以用行2
 
  1 p[2] = checked((byte)(p[2] + 20));
 
  2 p[2] = (byte)Math.Min (byte.MaxValue, p[2] + 20);
 
  大多处情况下,图像处理用的都是行2的做法,但是这样的话性能损失还是比较厉害的,需要计算一个最大值,还有一个类型的转换,我还在研究有什么更快的办法。还有一点,BMP 图像里面用的是小数端的存储方式(Little Endian),所以实际存储的图像顺序是GBR,这里要把图像的红色分量增加一些,改变的是第三个字节而不是第一个。
 
  最后两行就不说了,处理完毕需要Unlockbitmap必须被dispose掉。
 
  用以上代码进行图像处理的速度已经飞快了,最慢的部分就是调用那个LockBits的函数,这个速度基本上跟GDI是差不多的。但是如果你还是觉得慢,那还有以下2种办法可以提高性能,不过它已经远远超过GDI+的范畴了。
 
  1.别用GDI+的方法读图像,自己把图像文件读出来。如果是BMP倒是好办,要是用 JPGTIFPNGGIF....如果你足够牛,可以随手写个FFT或者DCT,那我倒是建议可以自己写写看,把JPG解压缩出来,自己修改修改再压缩回去。要是PNG/GIF/GIF 98,就是那个会动的 GIF,我就无语了。GIF的标准是公开的,不过时人家的专利,你写了是要付钱的。所以,还是算了吧。自己读BMP可以非常快地把图像处理掉,连LockBits都不需要。
 
  2.如果图像超级大,你又用了后面的行2进行处理,你对性能的要求又无比苛刻,那还是有条路可以走。直接用MMX/SSE/3D Now指令集,4个字节同时上。你去谢谢Intel/Amd给了那么好的指令集把,可惜C#你就别想了,没有办法直接用的。自己用C++调用MMX指令集写个 Dll,然后用interop调用。Interop的过程当中也是有损失的,Managed的内存空间用C#里面Marshal是需要被转换的。所以如果真的要这样做,我推荐使用C++.NET,两块东西都能写。这里我就不给出用MMX/SSE的例子了。最后再提一句,MMX对于图像处理也足够了,SSE主要是给浮点运算的,如果你有图像渲染的那种,用SSE会快很多,那是图形学的内容了,偶不懂,不多说,怕被人用棒子打。
 
  相关阅读
 
 

小编工资已与此挂钩!一一分钱!求打赏↓ ↓ ↓

如果你喜欢本文章,请赐赏:

已赐赏的人
最新评论(共0条)评论一句