在c#中,进行图像处理时主要会使用到 system.drawing
命名空间中的几个关键类,其中bitmap、bitmapdata和intptr是进行高效像素操作的重要工具。以下是如何利用这些类进行图像处理的方法概述:
-
bitmap 类:
system.drawing.bitmap
是一个封装了位图数据的类,它允许你加载、保存、显示和操作图像文件或内存中的位图资源。- 使用
new bitmap(width, height, pixelformat)
创建一个新的空白位图。 - 通过
bitmap.fromfile(path)
或bitmap.fromstream(stream)
加载图片文件。 - 提供
getpixel(x, y)
和setpixel(x, y, color)
方法用于获取和设置单个像素的颜色,但请注意,这种方法对于大规模图像处理效率较低。
- 使用
-
bitmapdata 类:
bitmapdata
代表了一个锁定的bitmap对象的像素数据缓冲区,可以直接对像素进行读写操作以提高性能。- 使用
bitmap.lockbits(rectangle area, imagelockmode mode, pixelformat format, out bitmapdata data)
方法可以锁定bitmap的部分或全部区域,从而获得指向该区域像素数据的指针。 data.scan0
属性是一个intptr类型,指向第一个像素的数据地址。- 解锁位图数据需要调用
bitmap.unlockbits(bitmapdata)
来释放资源并确保图形设备正确更新。
- 使用
-
intptr 类型与 marshal 类:
- intptr 是一个表示非托管指针(即内存地址)的数据类型。
- 在处理bitmapdata时,通常会将scan0属性所指向的内存块直接映射到托管代码中,以便于进行快速的像素操作。
- 使用
system.runtime.interopservices.marshal.copy(intptr source, byte[] destination, int startindex, int length)
将未托管的内存块内容复制到托管数组中,这样可以在c#中更方便地进行像素遍历和修改。 - 修改完成后,可以通过类似方法将修改后的数组内容复制回bitmapdata所指向的内存区域。
- 使用
综合以上,一个高效的图像处理流程可能是这样的:
- 创建或加载bitmap对象。
- 锁定bitmap的像素数据,得到bitmapdata。
- 使用marshal.copy将bitmapdata的像素数据复制到本地数组中。
- 对数组进行所需的图像处理操作(如:调整亮度、对比度、滤镜等)。
- 再次使用marshal.copy将处理过的数组数据复制回bitmapdata的内存中。
- 解锁bitmapdata,使得gdi+能够自动更新对应的bitmap图像。
这种方式避免了频繁调用getpixel和setpixel函数带来的性能瓶颈,实现了更快的图像处理速度。
bitmap类
命名空间:system.drawing
封装 gdi+ 位图,此位图由图形图像及其属性的像素数据组成。
bitmap 是用于处理由像素数据定义的图像的对象。
利用c#类进行图像处理,最方便的是使用bitmap类,使用该类的getpixel()与setpixel()来访问图像的每个像素点。下面是msdn中的示例代码:
public void getpixel_example(painteventargs e)
{
// create a bitmap object from an image file.
bitmap mybitmap = new bitmap("grapes.jpg");
// get the color of a pixel within mybitmap.
color pixelcolor = mybitmap.getpixel(50, 50);
// fill a rectangle with pixelcolor.
solidbrush pixelbrush = new solidbrush(pixelcolor);
e.graphics.fillrectangle(pixelbrush, 0, 0, 100, 100);
}
可见,bitmap类使用一种优雅的方式来操作图像,但是带来的性能的降低却是不可忽略的。比如对一个800*600的彩色图像灰度化,其耗费的时间都要以秒为单位来计算。在实际项目中进行图像处理,这种速度是决对不可忍受的。
bitmapdata类
命名空间:system.drawing.imaging
指定位图图像的属性。bitmapdata 类由 bitmap 类的 lockbits 和 unlockbits 方法使用。不可继承。
好在我们还有bitmapdata类,通过bitmapdata bitmapdata lockbits ( )可将 bitmap 锁定到系统内存中。该类的公共属性有:
- width 获取或设置 bitmap 对象的像素宽度。这也可以看作是一个扫描行中的像素数。
- height 获取或设置 bitmap 对象的像素高度。有时也称作扫描行数。
- pixelformat
获取或设置返回此 bitmapdata 对象的 bitmap 对象中像素信息的格式。 - scan0 获取或设置位图中第一个像素数据的地址。它也可以看成是位图中的第一个扫描行。
- stride 获取或设置 bitmap 对象的跨距宽度(也称为扫描宽度)。
下面的msdn中的示例代码演示了如何使用 pixelformat、height、width 和 scan0 属性;lockbits 和 unlockbits 方法;以及 imagelockmode 枚举。
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 = bmpdata.stride * bmp.height;
byte[] rgbvalues = new byte[bytes];
// copy the rgb values into the array.
system.runtime.interopservices.marshal.copy(ptr, rgbvalues, 0, bytes);
// set every red value to 255.
for (int counter = 0; counter < rgbvalues.length; counter+=3)
rgbvalues[counter] = 255;
// copy the rgb values back to the bitmap
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);
}
或
bitmap bitmap = new bitmap("image.jpg");
bitmapdata bitmapdata = bitmap.lockbits(new rectangle(0, 0, bitmap.width, bitmap.height), imagelockmode.readwrite, pixelformat.format32bppargb);
// 对 bitmapdata 进行图像处理操作
bitmap.unlockbits(bitmapdata);
上面的代码演示了如何用数组的方式来访问一幅图像,而不在使用低效的getpixel()和setpixel()。
intptr
使用 intptr
可以直接操作图像的内存数据。
通过获取图像的句柄(intptr
),可以使用指针操作来访问和修改图像的像素值。
这种方法需要更高级的编程技能和对内存管理的了解。
bitmap bitmap = new bitmap("image.jpg");
intptr ptr = bitmap.lockbits(new rectangle(0, 0, bitmap.width, bitmap.height), imagelockmode.readwrite, pixelformat.format32bppargb);
// 使用 intptr 进行图像处理操作
bitmap.unlockbits(ptr);
unsafe代码
而在实际中上面的做法仍然不能满足我们的要求,图像处理是一种运算量比较大的操作,不同于我们写的一般的应用程序。我们需要的是一种性能可以同c++程序相媲美的图像处理程序。c++是怎么提高效率的呢,答曰:指针。幸运的是.net也允许我们使用指针,只能在非安全代码块中使用指针。何谓非安全代码?
为了保持类型安全,默认情况下,c# 不支持指针运算。不过,通过使用 unsafe 关键字,可以定义可使用指针的不安全上下文。在公共语言运行库 (clr) 中,不安全代码是指无法验证的代码。c# 中的不安全代码不一定是危险的,只是其安全性无法由 clr 进行验证的代码。因此,clr 只对在完全受信任的程序集中的不安全代码执行操作。如果使用不安全代码,由您负责确保您的代码不会引起安全风险或指针错误。不安全代码具有下列属性:
- 方法、类型和可被定义为不安全的代码块。
- 在某些情况下,通过移除数组界限检查,不安全代码可提高应用程序的性能。
- 当调用需要指针的本机函数时,需要使用不安全代码。
- 使用不安全代码将引起安全风险和稳定性风险。
- 在 c# 中,为了编译不安全代码,必须用 /unsafe 编译应用程序。
正如《c#语言规范》中所说无论从开发人员还是从用户角度来看,不安全代码事实上都是一种“安全”功能。不安全代码必须用修饰符 unsafe 明确地标记,这样开发人员就不会误用不安全功能,而执行引擎将确保不会在不受信任的环境中执行不安全代码。
以下代码演示如何借助bitmapdata类采用指针的方式来遍历一幅图像,这里的unsafe代码块中的代码就是非安全代码。
//创建图像
bitmap image = new bitmap( "c:\\images\\image.gif" );
//获取图像的bitmapdata对像
bitmapdata data = image.lockbits( new rectangle( 0 , 0 , image.width , image.height ) , imagelockmode.readwrite , pixelformat.format24bpprgb );
//循环处理
unsafe
{
byte* ptr = ( byte* )( data.scan0 );
for( int i = 0 ; i < data.height ; i ++ )
{
for( int j = 0 ; j < data.width ; j ++ )
{
// write the logic implementation here
ptr += 3;
}
ptr += data.stride - data.width * 3;
}
}
毫无疑问,采用这种方式是最快的,所以在实际工程中都是采用指针的方式来访问图像像素的。
字节对齐问题
上例中ptr += data.stride - data.width * 3,表示跨过无用的区域,其原因是图像数据在内存中存储时是按4字节对齐的,具体解释如下:
假设有一张图片宽度为6,假设是format24bpprgb格式的(每像素3字节,在以下的讨论中,除非特别说明,否则bitmap都被认为是24位rgb)。显然,每一行需要6*3=18个字节存储。对于bitmap就是如此。但对于bitmapdata,虽然data.width还是等于image.width,但大概是出于显示性能的考虑,每行的实际的字节数将变成大于等于它的那个离它最近的4的整倍数,此时的实际字节数就是stride。就此例而言,18不是4的整倍数,而比18大的离18最近的4的倍数是20,所以这个data.stride = 20。显然,当宽度本身就是4的倍数时,data.stride = image.width * 3。
画个图可能更好理解。r、g、b 分别代表3个原色分量字节,bgr就表示一个像素。为了看起来方便我在们每个像素之间插了个空格,实际上是没有的。x表示补足4的倍数而自动插入的字节。为了符合人类的阅读习惯我分行了,其实在计算机内存中应该看成连续的一大段。
|-------Stride-----------|
|-------Width---------|
scan0:
BGR BGR BGR BGR BGR BGR XX
BGR BGR BGR BGR BGR BGR XX
BGR BGR BGR BGR BGR BGR XX
首先用data.scan0找到第0个像素的第0个分量的地址,这个地址指向的是个byte类型,所以当时定义为byte* ptr。行扫描时,在当前指针位置(不妨看成当前像素的第0个颜色分量)连续取出三个值(3个原色分量。注意,0 1 2代表的次序是b g r。在取指针指向的值时,貌似p[n]和p += n再取p[0]是等价的),然后下移3个位置(ptr += 3,看成指到下一个像素的第0个颜色分量)。做过bitmap.width次操作后,就到达了bitmap.width * 3的位置,应该要跳过图中标记为x的字节了(共有stride - width * 3个字节),代码中就是 ptr += datain.stride - datain.width * 3。
通过阅读本文,相信你已经对使用c#进行图像处理可能用到的几种方法有了一个了解。至于采用哪种方式,取决于你的性能要求。其中第一种方式最优雅;第三种方式最快,但不是安全代码;第二种方式取了个折中,保证是安全代码的同时又提高了效率。熟悉c/c++编程的人可能会比较偏向于第三种方式,我个人也比较喜欢第三种方式。
参考 http://blog.sina.com.cn/s/blog_628821950100wh9w.html
发表评论