2009-10-13 13 views
37

J'ai besoin d'accéder à chaque pixel d'un bitmap, de travailler avec eux, puis de les enregistrer dans un bitmap. En utilisant Bitmap.GetPixel() et Bitmap.SetPixel(), mon programme s'exécute lentement.Travail rapide avec Bitmaps en C#

Comment puis-je convertir rapidement Bitmap à byte[] et à l'arrière?

J'ai besoin d'un byte[] avec length = (4 * width * height), contenant les données RGBA de chaque pixel.

+1

Le lien suivant contient des méthodes d'accès aux pixels bitmap de manière comparative. http://csharpexamples.com/fast-image-processing-c/ – turgay

Répondre

65

Vous pouvez le faire de deux manières différentes. Vous pouvez utiliser unsafe pour accéder directement aux données ou vous pouvez utiliser marshaling pour copier les données. Le code dangereux est plus rapide, mais le marshaling ne nécessite pas de code dangereux. Voici un performance comparison que j'ai fait il y a quelques temps.

Voici un exemple complet utilisant LockBits:

/*Note unsafe keyword*/ 
public unsafe Image ThresholdUA(float thresh) 
{ 
    Bitmap b = new Bitmap(_image);//note this has several overloads, including a path to an image 

    BitmapData bData = b.LockBits(new Rectangle(0, 0, _image.Width, _image.Height), ImageLockMode.ReadWrite, b.PixelFormat); 

    byte bitsPerPixel = GetBitsPerPixel(bData.PixelFormat); 

    /*This time we convert the IntPtr to a ptr*/ 
    byte* scan0 = (byte*)bData.Scan0.ToPointer(); 

    for (int i = 0; i < bData.Height; ++i) 
    { 
     for (int j = 0; j < bData.Width; ++j) 
     { 
      byte* data = scan0 + i * bData.Stride + j * bitsPerPixel/8; 

      //data is a pointer to the first byte of the 3-byte color data 
     } 
    } 

    b.UnlockBits(bData); 

    return b; 
} 

Voici la même chose, mais avec marshaling:

/*No unsafe keyword!*/ 
public Image ThresholdMA(float thresh) 
{ 
    Bitmap b = new Bitmap(_image); 

    BitmapData bData = b.LockBits(new Rectangle(0, 0, _image.Width, _image.Height), ImageLockMode.ReadWrite, b.PixelFormat); 

    /* GetBitsPerPixel just does a switch on the PixelFormat and returns the number */ 
    byte bitsPerPixel = GetBitsPerPixel(bData.PixelFormat); 

    /*the size of the image in bytes */ 
    int size = bData.Stride * bData.Height; 

    /*Allocate buffer for image*/ 
    byte[] data = new byte[size]; 

    /*This overload copies data of /size/ into /data/ from location specified (/Scan0/)*/ 
    System.Runtime.InteropServices.Marshal.Copy(bData.Scan0, data, 0, size); 

    for (int i = 0; i < size; i += bitsPerPixel/8) 
    { 
     double magnitude = 1/3d*(data[i] +data[i + 1] +data[i + 2]); 

     //data[i] is the first of 3 bytes of color 

    } 

    /* This override copies the data back into the location specified */ 
    System.Runtime.InteropServices.Marshal.Copy(data, 0, bData.Scan0, data.Length); 

    b.UnlockBits(bData); 

    return b; 
} 
+1

Merci, et je n'avais même pas besoin d'aiguillonner quelqu'un. :) – MusiGenesis

+8

Vous devez savoir que l'ordre des canaux de couleur peut être différent selon le format PixelFormat que vous utilisez. Par exemple, avec les bitmaps 24 bits, le premier octet du pixel est le canal bleu, suivi du vert, puis du rouge, par opposition à l'ordre rouge-vert-bleu généralement attendu. – Jargon

+0

Merci pour votre effort! –

3

Vous voulez LockBits. Vous pouvez ensuite extraire les octets que vous voulez de l'objet BitmapData qu'il vous donne.

+0

Je pense que c'est plus rapide si vous utilisez l'objet 'BitmapData' renvoyé par LockBits dans un bloc' unsafe' avec un cast de pointeur (NOTE: Je n'en ai aucune idée si c'est plus rapide ou non, mais j'essaie de pousser quelqu'un d'autre à l'évaluer). – MusiGenesis

+1

Cela vous permettrait d'économiser les copies dans et hors de la BitmapData, ce qui est agréable. Je ne sais pas exactement combien d'économies seraient accordées dans la pratique; memcpy() est vraiment rapide ces jours-ci. –

1

bâtiment sur la réponse @notJim (et avec l'aide de http://www.bobpowell.net/lockingbits.htm), j'ai développé le ce qui rend ma vie beaucoup plus facile dans la mesure où je me retrouve avec un tableau de tableaux qui me permet de sauter à un pixel par ses coordonnées x et y. Bien sûr, la coordonnée x doit être corrigée par le nombre d'octets par pixel, mais c'est une extension facile.

Dim bitmapData As Imaging.BitmapData = myBitmap.LockBits(New Rectangle(0, 0, myBitmap.Width, myBitmap.Height), Imaging.ImageLockMode.ReadOnly, myBitmap.PixelFormat) 

Dim size As Integer = Math.Abs(bitmapData.Stride) * bitmapData.Height 
Dim data(size - 1) As Byte 

Marshal.Copy(bitmapData.Scan0, data, 0, size) 

Dim pixelArray(myBitmap.Height)() As Byte 

'we have to load all the opacity pixels into an array for later scanning by column 
'the data comes in rows 
For y = myBitmap.Height - 1 To 0 Step -1 
    Dim rowArray(bitmapData.Stride) As Byte 
    Array.Copy(data, y * bitmapData.Stride, rowArray, 0, bitmapData.Stride) 
    'For x = myBitmap.Width - 1 To 0 Step -1 
    ' Dim i = (y * bitmapData.Stride) + (x * 4) 
    ' Dim B = data(i) 
    ' Dim G = data(i + 1) 
    ' Dim R = data(i + 2) 
    ' Dim A = data(i + 3) 
    'Next 
    pixelArray(y) = rowArray 
Next 
0

Il existe une autre méthode beaucoup plus rapide et beaucoup plus pratique. Si vous regardez les constructeurs de Bitmap, vous en trouverez un qui prendra IntPtr comme dernier paramètre. IntPtr est destiné à contenir des données de pixels. Alors, comment l'utilisez-vous?

Dim imageWidth As Integer = 1920 
Dim imageHeight As Integer = 1080 

Dim fmt As PixelFormat = PixelFormat.Format32bppRgb 
Dim pixelFormatSize As Integer = Image.GetPixelFormatSize(fmt) 

Dim stride As Integer = imageWidth * pixelFormatSize 
Dim padding = 32 - (stride Mod 32) 
If padding < 32 Then stride += padding 

Dim pixels((stride \ 32) * imageHeight) As Integer 
Dim handle As GCHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned) 
Dim addr As IntPtr = Marshal.UnsafeAddrOfPinnedArrayElement(pixels, 0) 

Dim bitmap As New Bitmap(imageWidth, imageHeight, stride \ 8, fmt, addr) 

Ce que vous avez maintenant est un tableau entier simple et un bitmap référençant la même mémoire. Toutes les modifications que vous apportez au tableau Integer affecteront directement le bitmap. Essayons cela avec une simple transformation de luminosité.

Public Sub Brightness(ByRef pixels() As Integer, ByVal scale As Single) 
    Dim r, g, b As Integer 
    Dim mult As Integer = CInt(1024.0f * scale) 
    Dim pixel As Integer 

    For i As Integer = 0 To pixels.Length - 1 
     pixel = pixels(i) 
     r = pixel And 255 
     g = (pixel >> 8) And 255 
     b = (pixel >> 16) And 255 

     'brightness calculation 
     'shift right by 10 <=> divide by 1024 
     r = (r * mult) >> 10 
     g = (g * mult) >> 10 
     b = (b * mult) >> 10 

     'clamp to between 0 and 255 
     If r < 0 Then r = 0 
     If g < 0 Then g = 0 
     If b < 0 Then b = 0 
     r = (r And 255) 
     g = (g And 255) 
     b = (b And 255) 

     pixels(i) = r Or (g << 8) Or (b << 16) Or &HFF000000 
    Next 
End Sub 

Vous remarquerez peut-être que je l'ai utilisé une petite astuce pour éviter de faire des mathématiques à virgule flottante dans la boucle. Cela améliore les performances un peu. Et quand vous avez terminé, vous devez nettoyer un peu bien sûr ...

addr = IntPtr.Zero 
If handle.IsAllocated Then 
    handle.Free() 
    handle = Nothing 
End If 
bitmap.Dispose() 
bitmap = Nothing 
pixels = Nothing 

J'ai ignoré le composant alpha ici, mais vous êtes libre d'utiliser aussi. J'ai jeté ensemble beaucoup d'outils d'édition bitmap de cette façon. Il est beaucoup plus rapide et plus fiable que Bitmap.LockBits() et le meilleur de tous, il ne nécessite aucune copie de mémoire pour commencer à éditer votre bitmap.

3

Vous pouvez utiliser la méthode Bitmap.LockBits. De même, si vous souhaitez utiliser l'exécution de tâches parallèles, vous pouvez utiliser la classe Parallel dans l'espace de noms System.Threading.Tasks. Les liens suivants ont quelques exemples et explications.

0

Essayez cette solution C#.

Créez une application WinForms pour les tests.

Ajoutez un bouton et un PictureBox, ainsi qu'un événement de clic et un événement de fermeture de formulaire.

Utilisez le code suivant pour votre formulaire:

public partial class Form1 : Form 
{ 
    uint[] _Pixels { get; set; } 

    Bitmap _Bitmap { get; set; } 

    GCHandle _Handle { get; set; } 

    IntPtr _Addr { get; set; } 


    public Form1() 
    { 
     InitializeComponent(); 

     int imageWidth = 100; //1920; 

     int imageHeight = 100; // 1080; 

     PixelFormat fmt = PixelFormat.Format32bppRgb; 

     int pixelFormatSize = Image.GetPixelFormatSize(fmt); 

     int stride = imageWidth * pixelFormatSize; 

     int padding = 32 - (stride % 32); 

     if (padding < 32) 
     { 
      stride += padding; 
     } 

     _Pixels = new uint[(stride/32) * imageHeight + 1]; 

     _Handle = GCHandle.Alloc(_Pixels, GCHandleType.Pinned); 

     _Addr = Marshal.UnsafeAddrOfPinnedArrayElement(_Pixels, 0); 

     _Bitmap = new Bitmap(imageWidth, imageHeight, stride/8, fmt, _Addr); 

     pictureBox1.Image = _Bitmap; 

    } 

    private void button1_Click(object sender, EventArgs e) 
    { 
     for (int i = 0; i < _Pixels.Length; i++) 
     { 
      _Pixels[i] = ((uint)(255 | (255 << 8) | (255 << 16) | 0xff000000)); 

     } 

    } 

    private void Form1_FormClosing(object sender, FormClosingEventArgs e) 
    { 
     _Addr = IntPtr.Zero; 

     if (_Handle.IsAllocated) 
     { 
      _Handle.Free(); 

     } 

     _Bitmap.Dispose(); 

     _Bitmap = null; 

     _Pixels = null; 

    } 

} 

Maintenant, toutes les modifications apportées au tableau mettra automatiquement à jour le Bitmap.

Vous devrez appeler la méthode refresh sur la zone d'image pour voir ces changements.