天天看点

vb.net用Marshal类在内存中处理图像

vb.net不能直接操作指针,只能用marshal类来进行托管内存和非托管内存之间的交互,但是这样也依然比用GetPixel和SetPixel速度快。

讲之前先介绍一些方法的用法

Bitmap.LockBits 方法

定义如下:

Public Function LockBits (rect As Rectangle, flags As ImageLockMode, format As PixelFormat) As BitmapData
           

参数

rect

Rectangle

一个 Rectangle 结构,指定要锁定的 Bitmap 部分。此处可以指定要操作图的区域有利于准确处理图像

flags

ImageLockMode

一个 ImageLockMode 枚举,指定 Bitmap 的访问级别(读/写),决定该内存是读还是写的还是读写兼备的。

format

PixelFormat

一个 PixelFormat 枚举,指定此 Bitmap 的数据格式。决定每个像素的格式,每个通道应该占用多少位

返回

BitmapData

包含有关此锁定操作信息的 BitmapData。储存位图相关信息,如下:

Height

获取或设置 Bitmap 对象的像素高度。 有时也称作扫描行数。

PixelFormat

获取或设置返回此 Bitmap 对象的 BitmapData 对象中像素信息的格式。

Reserved

保留。 请勿使用。

Scan0

获取或设置位图中第一个像素数据的地址。 它也可以看成是位图中的第一个扫描行。

Stride

获取或设置 Bitmap 对象的跨距宽度(也称为扫描宽度)。

Width

获取或设置 Bitmap 对象的像素宽度。 这也可以看作是一个扫描行中的像素数。

这里讲一下跨距,跨距这个概念,其实就是一个扫描行所占的字节数,那么每个像素所占的字节就是Stride/Width

因为有的位图像素格式没有A通道所以有的像素占3个字节有的像素占4个字节甚至8字节,取决于PixelFormate的位数。比如Format32bppArgb,此格式每个像素32位,分为四个通道Argb,每个均摊8位,1byte=8bit(8位)

其他格式以此类推…

**Bitmap.UnlockBits(BitmapData) 方法

从系统内存解锁此 Bitmap。

这个方法必须要调用,否则你处理完图片后,在其他地方无法访问这图片所在的内存,也就会没法使用

Marshal.Copy 方法

托管内存与非托管内存之间的拷贝

可以理解为托管内存是.net管理(如我们自己声明的自定义类型和基本数据类型)的,非托管内存是系统管理的,比如句柄,Gdi+这些,都属于非托管对象,释放都得用相应的api释放。

这里用它的目的是将图片这个GDI对象的内存拷贝出来变成字节数组,以便我们进行操作

最重要的,Argb在内存中的位置

在内存中像素是怎么储存的呢,上面我们说到像素有Argb通道或者rgb,这里以Format32bppArgb 为例,它一个像素在内存中占用四个字节,每个通道一个字节,如下图展示3个像素

vb.net用Marshal类在内存中处理图像

可以看到在内存中的顺序和我们说的ARGB排列顺序不同,它是BGRA刚好倒过来,没有A通道就是BGR这个顺序。在我们用Marshal的拷贝方法拷贝得到的是一个字节数组,这个数组是一维的也就是和上面图一样呈线性。数组的元素排列顺序如同图上的顺序,4字节1像素。每个元素的值均是对应上面BGRA各个通道中的值,范围0-255。

但是显然我们的图像是二维的,所以这样不利于我们理解,所以我们把一维转换为二维,如图

vb.net用Marshal类在内存中处理图像

上面仅仅演示一个图片2*5大小的像素以及各个通道在内存中的排列,很容易我们能得到BGRA它们在内存中的公式。

这里因为选用的是32位的,所以,跨距/width=4,上面的公式可以改写成

vb.net用Marshal类在内存中处理图像

OK,有了上面的基础知识,来快活的处理图像吧。

一维处理方法

Public Sub 处理图片(ByVal btm As Bitmap)
  '将位图锁入系统内存中返回一个位图数据
  Dim btmdata = btm.LockBits(New Rectangle(0, 0, btm.Width, btm.Height), ImageLockMode.ReadWrite, btm.PixelFormat) 
  
        Dim ptrscan = btmdata.Scan0 '获取第一个扫描行的内存首地址
        Dim bytes(Math.Abs(btmdata.Stride) * btm.Height - 1) As Byte '声明一个托管内存数组,因为跨距可能为负所以这里用绝对值求数组长度
        Marshal.Copy(ptrscan, bytes, 0, bytes.Length) '将位图数据拷贝到托管数组中
        For i = 0 To bytes.Length - 1 Step 4   '这里把B通道值修改为255,因为是每个像素4字节,所以步进4
            bytes(i) = 255
        Next
 		 Marshal.Copy(bytes, 0, ptrscan, bytes.Length) '处理完后把处理的数据拷贝到非托管内存中即替换原来的位图数据
        btm.UnlockBits(btmdata) '解锁位图所在的内存以供我们访问

End Sub
           

二维处理方法(坐标处理法)

Public Sub 处理图片(ByVal btm As Bitmap)
  '将位图锁入系统内存中返回一个位图数据
  Dim btmdata = btm.LockBits(New Rectangle(0, 0, btm.Width, btm.Height), ImageLockMode.ReadWrite, btm.PixelFormat) 
  
        Dim ptrscan = btmdata.Scan0 '获取第一个扫描行的内存首地址
        Dim bytes(Math.Abs(btmdata.Stride) * btm.Height - 1) As Byte '声明一个托管内存数组,因为跨距可能为负所以这里用绝对值求数组长度
         Dim a As Integer = 0
        Dim b As Integer = 0
        Dim g As Integer = 0
        Dim r As Integer = 0
        '这里用声明一个匿名方法,只是为了使用便捷。
         Dim SetPix = Sub(x As Integer, y As Integer, pixColor As Color)
                         b = btmdata.Stride * x / btm.Width + btmdata.Stride * y ' If(btmdata.Stride / btm.Width = 4, a + 1, a)
                         g = b + 1
                         r = g + 1
                         a = r + 1
                         If bytes(a) > 0 Then '只修改非透明色的颜色
                             bytes(a) = pixColor.A
                             bytes(b) = pixColor.B
                             bytes(g) = pixColor.G
                             bytes(r) = pixColor.R
                         End If

                     End Sub
        For y = 0 To btm.Height - 1 
            For x = 0 To btm.Width - 1

                SetPix(x, y, Color.Blue) '将所有非透明色设置为蓝色
            Next
        Next
        Marshal.Copy(ptrscan, bytes, 0, bytes.Length) '将位图数据拷贝到托管数组中
      
 		 Marshal.Copy(bytes, 0, ptrscan, bytes.Length) '处理完后把处理的数据拷贝到非托管内存中即替换原来的位图数据
        btm.UnlockBits(btmdata) '解锁位图所在的内存以供我们访问

End Sub
           

以上便是使用marshal类处理图像的全过程,想要知道和SetPixel函数处理的速度差距,请自行测试。

虽然不能直接操作指针,但是因为能获取内存首地址可以通过内存偏移来得到其他内存地址,然后用marshal类的读写byte的方法进行直接操作内存不需要第二次的拷贝,但是差距和直接数组操作再拷贝的速度差别不是很大,具体测速可以看那位博主userbest我关注的列表里有他。

假如使用C#的指针直接操作不需要Marshal类将会更快,因为拷贝内存这个过程需要耗时,所以相对慢些。