Tutorial: LockBits

    • VB.NET

    Es gibt 4 Antworten in diesem Thema. Der letzte Beitrag () ist von LaMiy.

      Tutorial: LockBits

      Mein LockBits-Tutorial fange ich mal mit einer Gegenüberstellung an: Verschiedene Verfahren werden verwendet um eine Anzahl von Pixeln zu färben.



      GDI+ (.fillRectangle):....Sehr langsam (Hinweis: GDI ist schnell, hier nur absolut ungeeignet!)
      Bitmap.setPixel:..........Sehr langsam
      LockBits:.................Verdammt schnell


      Damit ist auch gleich die erste Frage beantwortet:
      Wer sollte Lockbits benutzen? Derjenige, der viele Pixel einzeln setzen oder abfragen möchte.

      Grundlagen
      Schaffen wir nun die Grundlage um LockBits zu verstehen:

      Farben: RGB, ARGB und Color
      Was eine Farbe ist weiß jeder - auch das .NET-Framework. Es stellt u.A. die Color-Struktur bereit um Farben handhabbar zu machen. Dabei wird auf den RGB-Farbraum zurückgegriffen. Eine Farbe wird durch vier Zahlen dargestellt: die drei Grundfarben (Rot, Grün und Blau), sowie den Alpha-Kanal welcher die Transparenz regelt.
      Jede dieser 4 Zahlen ist ein Byte und kann Werte von 0 bis 255 annehmen.

      PixelFormat: 32bpp-Bitmap und 24bpp-Bitmap
      Die Abkürzung bpp steht für BitsPerPixel. Wir erinnern uns daran, dass 1Byte=8Bits entspricht - 32bbp bedeutet also, dass 4 Byte auf einen Pixel kommen, bei 24bpp nur 3 Bytes. Im Endeffekt muss man nur wissen, dass bei 24bbp der Alpha-Kanal 'wegfällt' (=> Farben haben keine Tranzparenz, A=255) und nur die RGB-Werte relevant sind. Bei 32bpp kann man sich zusätzlich des Alpha-Kanals erfreuen.
      Hinweis: Es gibt auch weitere PixelFormate - für alle funktioniert LockBits ein wenig anders

      BitShifting
      Sehr nützlich können die BitShift-Operatoren werden. Eine Color kann durch 4 Bytes repräsentiert werden - ein Integer besteht auch aus 4 Bytes. Man kann jede ARGB-Farbe durch einen Integer darstellen (die einzelnen Farbkanäle zu einem Integer zusammen-shiften) oder aus einem Integer die einzelnen Kanäle herausfiltern. Es kann also durchaus sinnvoll werden einzelne Bits zu schubsen - das ist schnell und man spart sich den Weg über die langsamere Color-Struktur.
      Das ist aber alles Optimieren auf höchstem Niveau. Wer sich das noch nicht antun möchte kann auch die herkömmlichen Color-Methoden benutzen:

      VB.NET-Quellcode

      1. Dim eineFarbe As Color = Color.Black
      2. Dim farbeAlsInteger As Integer = eineFarbe.ToArgb
      3. Dim dieFarbe As Color = Color.FromArgb(farbeAlsInteger)


      Was macht das LockBits-Verfahren?
      Anschaulich gesagt: Das LockBits-Verfahren nimmt die Ziel-Bitmap und 'zerschneidet' sie in Streifen. Anschließend reiht es die Streifen aneinander. Aus einem ehemals zweidimensionalen Gebilde ist ein einfaches eindimensionales Byte-Array entstanden. Einzelne Bytes in einem Array zu lesen/setzen ist einfach und schnell. Der Clou: Das Byte-Array kann ganz einfach wieder zur Bitmap zusammengesetzt werden. Dieser Vorgang nennt sich UnlockBits.



      Praktisch gesehen passiert das meiste 'von alleine', denn die Bitmap samt der ganzen Bytes befinden sich schon im Hauptspeicher. Man muss sie nur rausfischen und später wieder zurückschieben.

      1. LockBits
      Durch LockBits wird die Bitmap gesperrt, sodass man an das zugrunde liegende Byte-Array heran kommt. Wichtig: Während die Bitmap gesperrt ist kann man sie nur eingeschränkt verwenden!

      VB.NET-Quellcode

      1. Dim input As Bitmap = eineBitmap '/new Bitmap(800,600) / etc
      2. Dim rect As New Rectangle(0, 0, input.Width, input.Height)
      3. Dim bmpData As System.Drawing.Imaging.BitmapData = input.LockBits(rect, Drawing.Imaging.ImageLockMode.WriteOnly, input.PixelFormat)
      4. Dim ptr As IntPtr = bmpData.Scan0
      5. Dim bytes As Integer = Math.Abs(bmpData.Stride) * input.Height
      6. Dim rgbValues(bytes - 1) As Byte
      7. System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes)

      Zu verstehen gibt es hier nicht viel. Eine Bitmap geht rein, das Byte-Array rgbValues kommt raus. Viel wichtiger ist:

      2. Manipulieren: Wie ist das Byte-Array aufgebaut und wie findet man die richtigen Bytes?

      1. Positionen gezielt anspringen
        Sehr häufig will man gezielt Pixel setzten oder auszulesen. Das Ziel ist also für gegebene x,y-Koordinaten die passenden Indizes zu finden. Schaut man sich an wie die Bitmap 'zerschnitten' wird (Abbildung oben) kommt man schnell auf eine einfache Formel um das Start-Bytes des zugehörigen Pixels ausfindig zu machen:

        Quellcode

        1. position = y * width * bytesPerPixel + x * bytesPerPixel

        x und y sind dabei die Koordinaten des Pixels, bytesPerPixel gibt an wie viele Bytes auf einen Pixel kommen (32bpp = 4 Bytes => ARGB) bzw (24bpp = 3 Bytes => RGB) und width ist die Breite der Bitmap.
        Eine Eigenheit: im Byte-Array sind ARGB bzw RGB genau umgekehrt hinterlegt.
        Hier mal anhand des Arrays einer 32bpp-Bitmap: B,G,R,A,B,G,R,A,B,G,R,A,B,G,R,A,B,G,R,A,...

        Ob man nun auslesen oder setzten möchte ist egal - einfach das Byte-Array manipulieren.

        VB.NET-Quellcode

        1. B = rgbValues(position + 0)
        2. G = rgbValues(position + 1)
        3. R = rgbValues(position + 2)
        4. A = rgbValues(position + 3)'Falls vorhanden!


      2. Array mit XY-Schleife durchlaufen
        Muss man jede Position genau einmal anspringen (z.B. um die Durchschnittsfarbe auszurechnen oder eine 2D-Kollisionsmap eines Spiels grafisch darzustellen) ist dieses Vorgehen vorteilhaft, da man nicht ständig den Index im Array berechnen muss und die Koordinaten quasi geschenkt bekommt.
        Wenn es noch schneller gehen muss kann man sogar parallel arbeitende Schleifen verwenden!

        VB.NET-Quellcode

        1. Dim offset As Integer = 0
        2. For y = 0 To input.Height - 1
        3. For x = 0 To input.Width - 1
        4. rgbValues(offset + 0) = B
        5. rgbValues(offset + 1) = G
        6. rgbValues(offset + 2) = R
        7. rgbValues(offset + 3) = A ' Falls vorhanden
        8. offset += 4 '<= BytesPerPixel!
        9. Next
        10. Next

        Auch hier gilt: einfach das Byte-Array manipulieren, egal ob man Bytes setzt oder liest.


      3. UnlockBits
      Nun soll das Byte-Array wieder zu der Bitmap zurück geschoben werden.

      VB.NET-Quellcode

      1. System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, rgbValues.Length)
      2. input.UnlockBits(bmpData)





      Das war es auch schon, eigentlich garkeine Zauberei^^
      Im Showroom gibt es btw fertige Libs die .GetPixel und .SetPixel auf LockBits-Grundlage implementiert haben.
      lg
      Hi
      für den Puffer kann man auch ein per System.Runtime.InteropServices.GCHandle angepinntes Integer-Array verwenden und dort die ARGB-Werte setzen. Dazu müssen das UserInputBuffer-Flag und ein zusätzliches System.Drawing.Imaging.BitmapData-Objekt angegeben werden beim Locken der Bitmap. Das GCHandle sollte immer freigegeben werden, also mit Try-Finally umrunden.
      Damit fällt dann das Marshal.Copy weg.
      Besser wäre auch dafür eine Argb-Struktur geeignet, die im Array bereitgestellt wird. Über explizit angegebenes StructLayout kannst du die einzelnen Felder kapseln. Das Struct selber muss eine Größe von 32 Bit haben, um 32 Bit Argbs aufzunehmen.

      Hier mal mein eigener Versuch noch:
      Spoiler anzeigen

      VB.NET-Quellcode

      1. Imports System.Runtime.InteropServices
      2. Imports System.Drawing.Imaging
      3. Public Delegate Function ColorEvaluator(x As Integer, y As Integer) As ArgbColor
      4. Public Delegate Function ColorFilter(x As Integer, y As Integer, previous As ArgbColor) As ArgbColor
      5. <StructLayout(Runtime.InteropServices.LayoutKind.Explicit)> _
      6. Public Structure ArgbColor
      7. <FieldOffset(0)> _
      8. Private _argb As Integer
      9. <FieldOffset(3)> _
      10. Private _a As Byte
      11. <FieldOffset(2)> _
      12. Private _r As Byte
      13. <FieldOffset(1)> _
      14. Private _g As Byte
      15. <FieldOffset(0)> _
      16. Private _b As Byte
      17. Public Sub New(argb As Integer)
      18. Me.Argb = argb
      19. End Sub
      20. Public Sub New(r As Byte, g As Byte, b As Byte)
      21. Me.A = 255
      22. Me.R = r
      23. Me.G = g
      24. Me.B = b
      25. End Sub
      26. Public Sub New(a As Byte, r As Byte, g As Byte, b As Byte)
      27. Me.A = a
      28. Me.R = r
      29. Me.G = g
      30. Me.B = b
      31. End Sub
      32. Public Property Argb As Integer
      33. Get
      34. Return _argb
      35. End Get
      36. Set(value As Integer)
      37. _argb = value
      38. End Set
      39. End Property
      40. Public Property R As Byte
      41. Get
      42. Return _r
      43. End Get
      44. Set(value As Byte)
      45. _r = value
      46. End Set
      47. End Property
      48. Public Property G As Byte
      49. Get
      50. Return _g
      51. End Get
      52. Set(value As Byte)
      53. _g = value
      54. End Set
      55. End Property
      56. Public Property B As Byte
      57. Get
      58. Return _b
      59. End Get
      60. Set(value As Byte)
      61. _b = value
      62. End Set
      63. End Property
      64. Public Property A As Byte
      65. Get
      66. Return _a
      67. End Get
      68. Set(value As Byte)
      69. _a = value
      70. End Set
      71. End Property
      72. Public Shared Widening Operator CType(value As Color) As ArgbColor
      73. Return New ArgbColor(value.A, value.R, value.G, value.B)
      74. End Operator
      75. Public Shared Narrowing Operator CType(value As ArgbColor) As Color
      76. Return Color.FromArgb(value.A, value.R, value.G, value.B)
      77. End Operator
      78. End Structure
      79. <Flags()>
      80. Public Enum AccessMode
      81. Read = 1
      82. Write = 2
      83. End Enum
      84. Public Class BitmapHelper
      85. Private _bitmap As Bitmap
      86. 'Operationsweite Daten
      87. Private _bitmapData As BitmapData
      88. Private _data() As ArgbColor
      89. Private _lockBounds As Rectangle
      90. Private _operationMode As AccessMode
      91. Public Sub New(bitmap As Bitmap)
      92. If bitmap Is Nothing Then Throw New ArgumentNullException("bitmap")
      93. If (bitmap.PixelFormat And PixelFormat.Indexed) = PixelFormat.Indexed Then
      94. Throw New ArgumentException("Indexed bitmap pixel formats are not supported.")
      95. End If
      96. Me.Bitmap = bitmap
      97. End Sub
      98. Public Property Bitmap As Bitmap
      99. Get
      100. Return _bitmap
      101. End Get
      102. Private Set(value As Bitmap)
      103. _bitmap = value
      104. End Set
      105. End Property
      106. Public Function BeginOperation() As IDisposable
      107. Return BeginOperation(AccessMode.Read Or AccessMode.Write)
      108. End Function
      109. Public Function BeginOperation(operationMode As AccessMode) As IDisposable
      110. If _data IsNot Nothing Then Throw New InvalidOperationException("Bitmap operation still in progress.")
      111. If (operationMode And (AccessMode.Read Or AccessMode.Write)) = 0 Then
      112. Throw New ArgumentException("Expected at least a read or write operation.")
      113. End If
      114. Dim width As Integer = Me.Bitmap.Width
      115. Dim height As Integer = Me.Bitmap.Height
      116. Dim data(width * height - 1) As ArgbColor 'Puffer mit allen Farbwerten anlegen
      117. Dim gch As GCHandle = GCHandle.Alloc(data, GCHandleType.Pinned) 'Anpinnen, um das Array über einen Zeiger ansteuern zu können
      118. Dim lockMode As ImageLockMode = ImageLockMode.UserInputBuffer
      119. If (operationMode And AccessMode.Read) = AccessMode.Read Then
      120. lockMode = lockMode Or ImageLockMode.ReadOnly
      121. End If
      122. If (operationMode And AccessMode.Write) = AccessMode.Write Then
      123. lockMode = lockMode Or ImageLockMode.WriteOnly
      124. End If
      125. _lockBounds = New Rectangle(0, 0, width, height) 'Bereich zwischenspeichern, der angefordert wurde
      126. Try
      127. Dim temporaryData As New BitmapData()
      128. temporaryData.Width = width
      129. temporaryData.Height = height
      130. temporaryData.PixelFormat = PixelFormat.Format32bppArgb
      131. temporaryData.Stride = width * 4
      132. 'Rohdaten-Zeiger auf die Adresse des Arrays legen, damit die Daten dorthin geladen werden
      133. temporaryData.Scan0 = gch.AddrOfPinnedObject()
      134. 'Daten anfordern, data enthält im Anschluss die Bitmapdaten als ArgbColor
      135. Me._bitmapData = Me.Bitmap.LockBits(_lockBounds, lockMode, PixelFormat.Format32bppArgb, temporaryData)
      136. Finally
      137. gch.Free()
      138. End Try
      139. _data = data
      140. Return New DisposeHandle(Me)
      141. End Function
      142. Private Sub EndOperation()
      143. CheckOperationStarted()
      144. Me.Bitmap.UnlockBits(Me._bitmapData)
      145. _data = Nothing
      146. Me._bitmapData = Nothing
      147. End Sub
      148. Public Sub Fill(value As ArgbColor)
      149. CheckOperationStarted()
      150. For i As Integer = 0 To _data.Length - 1
      151. _data(i) = value
      152. Next
      153. End Sub
      154. Public Sub FillAsync(value As ArgbColor)
      155. CheckOperationStarted()
      156. Dim pcc As Integer = Environment.ProcessorCount
      157. Dim data() As ArgbColor = _data
      158. Dim length As Integer = data.Length
      159. Parallel.For(0, pcc, Sub(i)
      160. Dim offs As Integer = length * i \ pcc
      161. For j As Integer = offs To offs + If(i = pcc - 1, data.Length - offs, length \ pcc)
      162. data(j) = value
      163. Next
      164. End Sub)
      165. End Sub
      166. Public Sub Filter(handler As ColorFilter)
      167. CheckOperationStarted()
      168. Dim width As Integer = _lockBounds.Width
      169. For i As Integer = 0 To _data.Length - 1
      170. _data(i) = handler(i Mod width, i \ width, _data(i))
      171. Next
      172. End Sub
      173. Public Sub Fill(handler As ColorEvaluator)
      174. CheckOperationStarted()
      175. Dim width As Integer = _lockBounds.Width
      176. For i As Integer = 0 To _data.Length - 1
      177. _data(i) = handler(i Mod width, i \ width)
      178. Next
      179. End Sub
      180. Private Sub CheckOperationStarted()
      181. If _data Is Nothing Then Throw New InvalidOperationException("Bitmap operation has not been started.")
      182. End Sub
      183. Private NotInheritable Class DisposeHandle
      184. Implements IDisposable
      185. Private _disposed As Boolean
      186. Private _helper As BitmapHelper
      187. Public Sub New(helper As BitmapHelper)
      188. _helper = helper
      189. End Sub
      190. Protected Sub Dispose(disposing As Boolean)
      191. If Not _disposed Then
      192. If disposing Then
      193. _helper.EndOperation()
      194. End If
      195. End If
      196. _disposed = True
      197. End Sub
      198. Public Sub Dispose() Implements IDisposable.Dispose
      199. Dispose(True)
      200. GC.SuppressFinalize(Me)
      201. End Sub
      202. End Class
      203. End Class

      Verwendung:

      VB.NET-Quellcode

      1. Dim sz As Size = New Size(1000, 1000)
      2. Dim bmph As New BitmapHelper(New Bitmap(sz.Width, sz.Height))
      3. Using bmph.BeginOperation()
      4. bmph.Fill(Function(x, y)
      5. x -= 50
      6. y -= 50
      7. '
      8. Return New ArgbColor(CByte(((Math.Sin(y - x * Math.Cos(y / 10)))) * Math.Sin((x * x + y * y - x * y) / 2500) * 63.75 + 63.75), _
      9. 0, _
      10. CByte((Math.Cos((x - y) / 5) * Math.Sin(y / 5) * Math.Cos(y / 5) / Math.Sqrt(2)) * 63.75 + 63.75))
      11. End Function)
      12. End Using
      13. Me.BackgroundImage = bmph.Bitmap


      Gruß
      ~blaze~

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „~blaze~“ ()

      Bin gerade auf ​GetBitmapBits gestoßen. Sieht auch ganz nett aus. LockBits scheint aber dennoch schneller zu sein.
      Diese Variante ist aber kürzer.
      pinvoke.net/default.aspx/gdi32.GetBitmapBits
      msdn.microsoft.com/de-de/libra…op/dd144850(v=vs.85).aspx

      C#-Quellcode

      1. /// <summary>
      2. /// The GetBitmapBits function copies the bitmap bits of a specified device-dependent bitmap into a buffer.
      3. /// Note: This function is provided only for compatibility with 16-bit versions of Windows. Applications should use the GetDIBits function.
      4. /// </summary>
      5. /// <param name="hbmp">A handle to the device-dependent bitmap.</param>
      6. /// <param name="cbBuffer">The number of bytes to copy from the bitmap into the buffer.</param>
      7. /// <param name="lpvBits">A pointer to a buffer to receive the bitmap bits. The bits are stored as an array of byte values.</param>
      8. /// <returns>If the function succeeds, the return value is the number of bytes copied to the buffer.
      9. /// If the function fails, the return value is zero.</returns>
      10. [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
      11. public static extern int GetBitmapBits(IntPtr hbmp, int cbBuffer, byte[] lpvBits);