量化GDI+:快速Bitmap读写像素
写在前面的话:
本文针对GDI+下Bitmap操作(Get/SetPixel)进行测试,而非寻求最快速的位图处理方式。如果你需要速度上的提升,请使用GDI+以外的技术,如并行计算、调用MMX/SSE指令、CUDA等。
这是一个古老的技巧:
使用Bitmap类时经常会用到GetPixel和SetPixel,但是这两个方法直接使用都比较慢,所以一般都会使用LockBits/UnlockBits将位图在内存中锁定,以加快操作速度。
MSDN上的标准参考是这样的:
private void LockUnlockBitsExample(PaintEventArgs e)
{
// Create a new bitmap.创建位图
Bitmap bmp = new Bitmap("c:\fakePhoto.jpg");
// Lock the bitmap's bits. 锁定位图
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
System.Drawing.Imaging.BitmapData bmpData =
bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
bmp.PixelFormat);
// Get the address of the first line.获取首行地址
IntPtr ptr = bmpData.Scan0;
// Declare an array to hold the bytes of the bitmap.定义数组保存位图
int bytes = Math.Abs(bmpData.Stride) * bmp.Height;
byte[] rgbValues = new byte[bytes];
// Copy the RGB values into the array.复制RGB值到数组
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);
// Set every third value to 255. A 24bpp bitmap will look red. 把每像素第3个值设为255.24bpp的位图将变红
for (int counter = 2; counter < rgbValues.Length; counter += 3)
rgbValues[counter] = 255;
// Copy the RGB values back to the bitmap 把RGB值拷回位图
System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);
// Unlock the bits.解锁
bmp.UnlockBits(bmpData);
// Draw the modified image.绘制更新了的位图
e.Graphics.DrawImage(bmp, 0, 150);
}
private void LockUnlockBitsExample(PaintEventArgs e)
{
// Create a new bitmap.创建位图
Bitmap bmp = new Bitmap("c:\fakePhoto.jpg");
// Lock the bitmap's bits. 锁定位图
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
System.Drawing.Imaging.BitmapData bmpData =
bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
bmp.PixelFormat);
// Get the address of the first line.获取首行地址
IntPtr ptr = bmpData.Scan0;
// Declare an array to hold the bytes of the bitmap.定义数组保存位图
int bytes = Math.Abs(bmpData.Stride) * bmp.Height;
byte[] rgbValues = new byte[bytes];
// Copy the RGB values into the array.复制RGB值到数组
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);
// Set every third value to 255. A 24bpp bitmap will look red. 把每像素第3个值设为255.24bpp的位图将变红
for (int counter = 2; counter < rgbValues.Length; counter += 3)
rgbValues[counter] = 255;
// Copy the RGB values back to the bitmap 把RGB值拷回位图
System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);
// Unlock the bits.解锁
bmp.UnlockBits(bmpData);
// Draw the modified image.绘制更新了的位图
e.Graphics.DrawImage(bmp, 0, 150);
}
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
tmp = bmp.GetPixel(x, y);
}
}
后记
有朋友指出:
GDI+这种 LockBits是临时性的把图像的数据读到内存,是不适合于做专业的图像处理软件的,专业做的话一个图像加载后在内存中的格式应该是固定的,这样做算法也就是直接访问这段内存的数据。GetPixel之类的函数的存在也不是为了专业的图像处理的,而是对类似于屏幕取色或DC取色这样小批量数据时方便处理。
要玩速度,图像处理方面的算法先是用普通语言写出来,对算法的核心尽心优化,如果速度还不行,考虑用汇编进一步优化,越简单的算法,用汇编优化的速度能提高的倍数越高,比如,最简单的反色算法,3000*4000*24的图像,一般的语言要100ms左右的处理时间,用汇编的话20ms够了,不过复杂的算法,一般汇编能提升的档次不会有这么明显。
尽管本文的目的并非追求速度,仅仅是「测试」速度,我还是尝试着优化了一下代码,就用上述「反色」操作为例进行了测试。
测试用例选择#7(4096x4096 @ 24bpp),用时299ms,截图如下:
随后再次改进算法,得到了52ms的速度提升(约17%)。
这个结果,尽管较一般操作方式快了不少,但和「一般的语言100ms左右」比起来,还是「右」得多了一点(笑)。和汇编的「20ms」(无实验数据)比,差得更远了。
优化代码似乎比较有趣,我就继续试着优化一下。通过调整调用结构,改进算法,使用多线程并行计算,总算是进入50ms了。
仍旧基于Bitmap类的LockBits/UnlockBits。
语言:C#、C#指针
测试机:i3 380M @2.53GHz,2.92G DDR3-1333,Windows 7 32位
速度:约50ms
网友测试结果对比
以下是部分热心网友给出的测试结果数据,加以对比,供诸君参考。
测试项目统一为对规格为4096x4096x24bpp的位图图像进行反色处理。
测试1:
ImageWizard(作者:laviewpbt)
实现:汇编+VB.NET
配置:i3 380M @2.53GHz,2.92G DDR3-1333,Windows 7 32位
用时:25ms
测试2:
临时测试(作者:兰征鹏)
实现:VC++.NET调用SSE指令
配置:i7 860@2.93GHz,12G PC1333内存,Windows7 64位
用时:12~19ms
测试3:
GebImage(作者:xiaotie)
实现:C#重写全部图像库、unsafe指针
配置:优于测试1
用时:33ms
测试4:
本文(作者:野比)
实现:GDI+、unsafe指针
配置:同测试1
用时:46ms
本文示例源码下载:
欢迎光临 CSkin博客 (http://bbs.cskin.net/) | Powered by Discuz! X3.2 |