天天看點

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類将會更快,因為拷貝記憶體這個過程需要耗時,是以相對慢些。