Version 2.0!
Features
Tutorials
Files
Glossary
Projects
Contact
Links
Message Board
Extras
LuckyCam
Old News
Sign Guestbook
View Guestbook
VB Horoscope
VB Photo Album
.
ATTENTION READERS! Lucky's VB Gaming Site is no longer active. For updated game programming information and tutorials, please visit The Game Programming Wiki!

Einzelne Pixel setzen

 

Hallo! Dieses Tutorial wurde von Stephan Kirchmaier geschrieben und Sie finden das Original auf www.vb-empire.de.vu. Es gibt mehrere Möglichkeiten, um einzelne Pixel zu setzen. Jedoch sind die meisten dieser Methoden ziemlich langsam. Ich werde in diesem Tutorial insgesamt vier Möglichkeiten vorstellen. Außerdem gibt es ein Beispielprogramm, dass die einzelnen Methoden vergleicht. Ich werde mich hier auf 24Bit-Bilder beschränken.
Zuerst sollte ein Bild in eine PictureBox geladen werden und die ScaleMode-Eigenschaft der PictureBox muss 3 (vbPixels) sein:

Set Picture1.Picture = LoadPicture(<Pfad zur Datei>)

Nun können wir beginnen:

1) PSet und Point

'Point' liest einen Wert von einer angegebenen Position aus. 'PSet' setzt einen Pixel in der angegebenen Farbe auf der angegebenen Position. Nun können wir das ganze Bild verändern:

For i = 0 To Picture1.ScaleWidth
    For j = 0 To Picture1.ScaleHeight
        Col = Picture1.Point(i, j)
        Col = Abs(Col) \ 2
        Picture1.PSet (i, j), Col
    Next j
Next i

Das Programm geht hier von der linken oberen Ecke spaltenweise runter, liest jeden Pixel, halbiert die Farbe und setzt ihn wieder mit der neuen Farbe. 'Abs(Col)' wird verwendet weil die Farbe auch -1 sein kann. Das bedeutet, dass die Farbe des Pixels nicht verfügbar ist.

2) SetPixel und GetPixel

Das sind zwei API-Funktionen. Beide machen im Prinzip nichts anderes als 'PSet' und 'Point' aber sie sind um einiges schneller. Hier die Deklarationen:

Declare Function GetPixel Lib "gdi32" Alias "GetPixel" (ByVal hdc As Long, ByVal x As Long, ByVal y As Long) As Long
Declare Function SetPixel Lib "gdi32" Alias "SetPixel" (ByVal hdc As Long, ByVal x As Long, ByVal y As Long, ByVal crColor As Long) As Long

Für diese zwei Funktionen braucht man den 'DeviceContext' der PictureBox. Diese ist in der hDC-Eigenschaft der PictureBox gespeichert.

For i = 0 To Picture1.ScaleWidth
    For j = 0 To Picture1.ScaleHeight
        col = GetPixel(Picture1.hdc, i, j)
        col = Abs(col) \ 2
        SetPixel Picture1.hdc, i, j, col
    Next j
Next i

Dieser Code wird jetzt um einiges schneller ausgeführt. Jedoch ist er für richtige Anwendungen noch immer zu langsam. Ein bisschen langsamer ist der Einsatz der 'SetPixelV'-Funktion anstatt der 'SetPixel'-Funktion. Diese arbeitet sonst gleich wie die 'SetPixel'-Funktion. Jedoch wird nicht die richtige Farbe angezeigt, sondern eine Annäherung an die Farbe. Man merkt den Unterschied jedoch nicht wirklich. 

Declare Function SetPixelV Lib "gdi32" Alias "SetPixelV" (ByVal hdc As Long, ByVal x As Long, ByVal y As Long, ByVal crColor As Long) As Long

3) SetPixel und GetPixel mit selbst erzeugten DC

Das ist ein ziemlich komplizierter Weg, der den Vorgang jedoch wieder um einige Millisekunden beschleunigt. Zuerst erzeugt man einen DeviceContext und ein kompatibles Bitmap. Dann selektiert man das Bitmap in den DeviceContext. Das bedeutet, dass auf dieses Bitmap über den DeviceContext zugegriffen werden kann. Dann kopiert man das gesamte Bild in das Bitmap und modifiziert es dort. Danach kopiert man das geänderte Bitmap wieder zurück. Dieser Weg ist schon wieder schneller aber schwieriger einzubauen:

Diese API-Funktionen werden benötigt:

Private Declare Function BitBlt Lib "gdi32" (ByVal hDestDC As Long, ByVal x As Long, ByVal y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal hSrcDC As Long, ByVal xSrc As Long, ByVal ySrc As Long, ByVal dwRop As Long) As Long
Private Declare Function CreateCompatibleBitmap Lib "gdi32" (ByVal hdc As Long, ByVal nWidth As Long, ByVal nHeight As Long) As Long
Private Declare Function CreateCompatibleDC Lib "gdi32" (ByVal hdc As Long) As Long
Private Declare Function SelectObject Lib "gdi32" (ByVal hdc As Long, ByVal hObject As Long) As Long
Private Declare Function DeleteDC Lib "gdi32" (ByVal hdc As Long) As Long
Private Declare Function DeleteObject Lib "gdi32" (ByVal hObject As Long) As Long

'BitBlt' kopiert die Bilder hin und wieder zurück.
'CreateCompatibleBitmap' erzeugt im Speicher Platz für ein neues Bitmap.
'CreateCompatibleDC' erzeugt den DeviceContext.
'SelectObject' selektiert das Bitmap in den DeviceContext.
'DeleteDC' gibt den erzeugten DeviceContext wieder frei.
'DeleteObject' gibt den Speicher, den das Bitmap belegt, wieder frei.

Dim mDC, mBMP

mDC = CreateCompatibleDC(Picture1.hdc)
mBMP = CreateCompatibleBitmap(Picture1.hdc, Picture1.ScaleWidth, Picture1.ScaleHeight)
SelectObject mDC, mBMP
BitBlt mDC, 0, 0, sw, sh, Picture1.hdc, 0, 0, vbSrcCopy
For i = 0 To Picture1.ScaleWidth
    For j = 0 To Picture1.ScaleHeight
        col = GetPixel(mDC, i, j)
        col = Abs(col) \ 2
        SetPixel mDC, i, j, col
    Next j
Next i
BitBlt Picture1.hdc, 0, 0, Picture1.ScaleWidth, Picture1.ScaleHeight, mDC, 0, 0, vbSrcCopy
DeleteObject mBMP
DeleteDC mDC

Auch hier kann man die 'SetPixelV'-Funktion anstatt 'SetPixel' verwenden.

4) Einen Pointer verwenden

Zuerst brauchen wir ein paar Deklarationen:

Option Explicit

Type SAFEARRAYBOUND
    cElements As Long
    lLbound As Long
End Type

Type SAFEARRAY2D
    cDims As Integer
    fFeatures As Integer
    cbElements As Long
    cLocks As Long
    pvData As Long
    Bounds(0 To 1) As SAFEARRAYBOUND
End Type

Type BITMAP
    bmType As Long
    bmWidth As Long
    bmHeight As Long
    bmWidthBytes As Long
    bmPlanes As Integer
    bmBitsPixel As Integer
    bmBits As Long
End Type

Declare Function VarPtrArray Lib "msvbvm50.dll" Alias "VarPtr" (Ptr() As Any) As Long
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDst As Any, pSrc As Any, ByVal ByteLen As Long)
Declare Function GetObjectAPI Lib "gdi32" Alias "GetObjectA" (ByVal hObject As Long, ByVal nCount As Long, lpObject As Any) As Long

Der UDT 'SAFEARRAY2D' wird intern von Visual Basic benutzt um mehrdimensionale Arrays zu verwalten.
Der UDT 'BITMAP' wird ein paar Informationen über das Bild bereithalten.
'VarPtrArray' gibt die interne Speicheradresse eines Arrays zurück.
'CopyMemory' kopiert blitzschnell Speicherblöcke von einer Position zur anderen.
'GetObjectAPI' gibt uns Informationen über das Bitmap, die dann im UDT 'BITMAP' gespeichert werden.

Dim pic() As Byte
Dim sa As SAFEARRAY2D
Dim bmp As BITMAP
Dim r As Long, g As Long, b As Long

Diese Variablen müssen deklariert werden. In 'pic()' wird das gesamte Bild gespeichert. 'sa' wird verwendet um Visual Basic einen Array vorzutäuschen. In 'bmp' werden die Informationen über das Bild gespeichert. 'r', 'g' und 'b' beinhalten die Rot-, Grün- und Blauwerte, die aus 'pic()' ausgelesen werden.

GetObjectAPI Picture1.Picture, Len(bmp), bmp

Jetzt haben wir die Informationen über das Bild gespeichert.

With sa
    .cbElements = 1
    .cDims = 2
    .Bounds(0).lLbound = 0
    .Bounds(0).cElements = bmp.bmHeight
    .Bounds(1).lLbound = 0
    .Bounds(1).cElements = bmp.bmWidthBytes
    .pvData = bmp.bmBits
End With

Nun füllen wir 'sa'. Es wird hier ein zweidimensionaler Array erstellt. Die obere Grenze der ersten Dimension ist die Höhe des Bild in Pixel. Die obere Grenze der zweiten Dimension ist drei mal die Breite des Bildes. Man muss bedenken, dass die Farben der Pixel sich aus drei Teilen zusammensetzen. 'sa.pvData' zeigt jetzt auf die Daten des Bitmaps.

CopyMemory ByVal VarPtrArray(pic), VarPtr(sa), 4

Jetzt muss man 'pic()' mit dem selbstgemachten Array überschreiben.

For i = 0 To UBound(pic, 1)
    For j = 0 To UBound(pic, 2)
        pic(i, j) = 255 - pic(i, j)
    Next j
Next i

Jetzt braucht man zwei Schleifen, um den Array zu verändern. Das obere Beispiel invertiert das Bild. Wollen Sie dir Rot-, Grün- und Blauwerte separat bearbeiten müssen Sie so vorgehen:

For i = 0 To UBound(pic, 1) - 3 Step 3
    For j = 0 To UBound(pic, 2)
        r = pic(i + 2, j)
        g = pic(i + 1, j)
        b = pic(i, j)
        r = ((g * b) \ 128)
        g = ((r * b) \ 128)
        b = ((r * g) \ 128)
        If r > 255 Then r = 255
        If r < 0 Then r = 0
        If g > 255 Then g = 255
        If g < 0 Then g = 0
        If b > 255 Then b = 255
        If b < 0 Then b = 0
        pic(i, j) = b
        pic(i + 1, j) = g
        pic(i + 2, j) = r
    Next j
Next i

Interessant ist jetzt noch wie die Bilddaten im Array gespeichert werden. Diese werden von der linken unteren Ecke zur rechten oberen gespeichert. Die folgende Tabelle verdeutlicht das:

1 b g r b g r b g r
0 b g r b g r b g r
  0 1 2 3 4 5 6 7 8

pic(0, 0)  beinhaltet also den Blauwert des linken unteren Pixels.
RGB(pic(0, 2), pic(0,1), pic(0,0)) ist die Farbe des linken unteren Pixels.

CopyMemory ByVal VarPtrArray(pic), 0&, 4

Nachdem man den Array bearbeitet hat sollte man diesen mit der oben genannten Zeile löschen.

Picture1.Refresh

Da man das Bild direkt im Speicher verändert hat, muss man noch das Bild neu zeichnen lassen.

Mit dieser Methode ist richtig schnelle Manipulation an einzelne Pixel eines Bildes möglich. Man kann diese Methode auch in Spielen verwenden.
Ich habe hier eine Tabelle erstellt, die den Geschwindigkeitsunterschied zeigen soll. Das Bild wurde immer invertiert und ist 433x263 Pixel gross. Mein PC ist ein P2 MMX mit 300 MHz und 96 MB Ram. Jeder Test wurde fünfmal wiederholt und dann wurde der Durchschnitt ausgerechnet:

PSet und Point 3737,6 ms
GetPixel und SetPixel 3133,4 ms
GetPixel und SetPixelV 3210,4 ms
GetPixel und SetPixel mit selbsterstelltem DC 2032,4 ms
GetPixel und SetPixelV mit selbsterstelltem DC 1936,0 ms
Pointer 222,0 ms

Man sieht, dass die 'Pointer-Methode' um einiges schneller ist als alle anderen. Deshalb sollten alle Programme, die das benötigen mit dieser Methode arbeiten.


Falls Sie irgendwelche Anfragen, Beschwerden oder Vorschläge haben: VB_Empire@gmx.at