web-dev-qa-db-fra.com

Créer un bitmap à partir d'un tableau d'octets de données de pixels

Cette question concerne la façon de lire/écrire, d'allouer et de gérer les données de pixels d'un bitmap.

Voici un exemple de la façon d'allouer un tableau d'octets (mémoire gérée) aux données de pixels et de créer un bitmap à l'aide de celui-ci:

Size size = new Size(800, 600);
PixelFormat pxFormat = PixelFormat.Format8bppIndexed;
//Get the stride, in this case it will have the same length of the width.
//Because the image Pixel format is 1 Byte/pixel.
//Usually stride = "ByterPerPixel"*Width

// Mais ce n'est pas toujours vrai. Plus d'informations sur bobpowell .

int stride = GetStride(size.Width, pxFormat);
byte[] data = new byte[stride * size.Height];
GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
Bitmap bmp = new Bitmap(size.Width, size.Height, stride,
             pxFormat, handle.AddrOfPinnedObject());

//After doing your stuff, free the Bitmap and unpin the array.
bmp.Dispose();
handle.Free();

public static int GetStride(int width, PixelFormat pxFormat)
{
    //float bitsPerPixel = System.Drawing.Image.GetPixelFormatSize(format);
    int bitsPerPixel = ((int)pxFormat >> 8) & 0xFF;
    //Number of bits used to store the image data per line (only the valid data)
    int validBitsPerLine = width * bitsPerPixel;
    //4 bytes for every int32 (32 bits)
    int stride = ((validBitsPerLine + 31) / 32) * 4;
    return stride;
}

Je pensais que le Bitmap ferait une copie des données du tableau, mais il pointe en fait vers les mêmes données. Étiez-vous en mesure de voir:

Color c;
c = bmp.GetPixel(0, 0);
Console.WriteLine("Color before: " + c.ToString());
//Prints: Color before: Color [A=255, R=0, G=0, B=0]
data[0] = 255;
c = bmp.GetPixel(0, 0);
Console.WriteLine("Color after: " + c.ToString());
//Prints: Color after: Color [A=255, R=255, G=255, B=255]

Questions:

  1. Est-il sûr de créer un bitmap à partir d'un tableau d'octets [] (mémoire gérée) et de libérer () le GCHandle? Si ce n'est pas sûr, je dois garder un tableau épinglé, à quel point cela est-il mauvais pour GC/Performance?

  2. Est-il sûr de modifier les données (ex: data [0] = 255;)?

  3. L'adresse d'un Scan0 peut être modifiée par le GC? Je veux dire, j'obtiens le Scan0 à partir d'un bitmap verrouillé, puis le déverrouille et après un certain temps le verrouiller à nouveau, le Scan0 peut être différent?

  4. À quoi sert ImageLockMode.UserInputBuffer dans la méthode LockBits? Il est très difficile de trouver des informations à ce sujet! MSDN ne l'explique pas clairement!

EDIT 1: Quelques suivis

  1. Vous devez le garder épinglé. Cela ralentira-t-il le GC? je l'ai demandé ici . Cela dépend du nombre d'images et de ses tailles. Personne ne m'a donné de réponse quantitative. Cela semble difficile à déterminer. Vous pouvez également allouer la mémoire à l'aide de Marshal ou utiliser la mémoire non gérée allouée par le bitmap.

  2. J'ai fait beaucoup de tests en utilisant deux threads. Tant que le bitmap est verrouillé, il est correct. Si le bitmap est déverrouillé, il n'est pas sûr! Mon article sur la lecture/écriture directement sur Scan . Boing's answer "J'ai déjà expliqué ci-dessus pourquoi vous êtes chanceux de pouvoir utiliser scan0 en dehors du verrou. Parce que vous utilisez le PixelFormat bmp d'origine et que GDI est optimisé dans ce cas pour vous donner le pointeur et non une copie. Ce pointeur est valide jusqu'à ce que le système d'exploitation décide de le libérer. La seule fois où il y a une garantie est entre LockBits et UnLockBits. Point. "

  3. Oui, cela peut arriver, mais les grandes zones de mémoire sont traitées différemment par le GC, il déplace/libère ce grand objet moins fréquemment. Il peut donc falloir un certain temps au GC pour déplacer ce tableau. De MSDN : "Toute allocation supérieure ou égale à 85,000 bytes Va sur la large object heap (LOH)" ... "LOH n'est collectée que lors d'une collecte de génération 2". .NET 4.5 a des améliorations dans LOH.

  4. @Boing a répondu à cette question. Mais je vais l'admettre. Je ne l'ai pas bien compris. Donc si Boing ou quelqu'un d'autre pouvait please clarify it, Je serais content. Au fait, pourquoi je ne peux pas simplement lire/écrire directement sur Sca0 sans verrouiller ? => Vous ne devez pas écrire directement dans Scan0 car Scan0 pointe vers une copie des données Bitmap créées par la mémoire non gérée (à l'intérieur de GDI). Après le déverrouillage, cette mémoire peut être réaffectée à d'autres éléments, il n'est plus certain que Scan0 pointera vers les données Bitmap réelles. Cela peut être reproduit en obtenant le Scan0 dans un verrou, déverrouiller et faire une rotation-flit dans le bitmap déverrouillé. Après un certain temps, Scan0 pointera vers une région non valide et vous obtiendrez une exception lorsque vous essayez de lire/écrire sur son emplacement mémoire.

37
Pedro77
  1. Son sûr si vous marshalez les données.copy plutôt que de définir scan0 (directement ou via cette surcharge de BitMap ()). Vous ne voulez pas épingler les objets gérés, cela contraindra le garbage collector.
  2. Si vous copiez, parfaitement sûr.
  3. Le tableau d'entrée est géré et peut être déplacé par le CPG, scan0 est un pointeur non géré qui serait obsolète si le tableau était déplacé. L'objet Bitmap lui-même est géré mais définit le pointeur scan0 dans Windows via un handle.
  4. ImageLockMode.UserInputBuffer est? Apparemment, il peut être transmis à LockBits, il indique peut-être à Bitmap () de copier les données du tableau d'entrée.

Exemple de code pour créer un bitmap en niveaux de gris à partir d'un tableau:

    var b = new Bitmap(Width, Height, PixelFormat.Format8bppIndexed);

    ColorPalette ncp = b.Palette;
    for (int i = 0; i < 256; i++)
        ncp.Entries[i] = Color.FromArgb(255, i, i, i);
    b.Palette = ncp;

    var BoundsRect = new Rectangle(0, 0, Width, Height);
    BitmapData bmpData = b.LockBits(BoundsRect,
                                    ImageLockMode.WriteOnly,
                                    b.PixelFormat);

    IntPtr ptr = bmpData.Scan0;

    int bytes = bmpData.Stride*b.Height;
    var rgbValues = new byte[bytes];

    // fill in rgbValues, e.g. with a for loop over an input array

    Marshal.Copy(rgbValues, 0, ptr, bytes);
    b.UnlockBits(bmpData);
    return b;
24
Peter Wishart

Concernant votre question 4: Le ImageLockMode.UserInputBuffer peut vous donner le contrôle du processus d'allocation de cette énorme quantité de mémoire qui pourrait être référencée dans un objet BitmapData.

Si vous choisissez de créer vous-même l'objet BitmapData, vous pouvez éviter un Marshall.Copy. Vous devrez ensuite utiliser cet indicateur en combinaison avec un autre ImageLockMode.

Attention, c'est une affaire compliquée, notamment concernant Stride et PixelFormat.

Voici un exemple qui obtiendrait en un coup le contenu du tampon 24bbp sur un BitMap puis dans un autre plan le relire dans un autre tampon en 48bbp.

Size size = Image.Size;
Bitmap bitmap = Image;
// myPrewrittenBuff is allocated just like myReadingBuffer below (skipped for space sake)
// But with two differences: the buff would be byte [] (not ushort[]) and the Stride == 3 * size.Width (not 6 * ...) because we build a 24bpp not 48bpp
BitmapData writerBuff= bm.LockBits(new Rectangle(0, 0, size.Width, size.Height), ImageLockMode.UserInputBuffer | ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb, myPrewrittenBuff);
// note here writerBuff and myPrewrittenBuff are the same reference
bitmap.UnlockBits(writerBuff);
// done. bitmap updated , no marshal needed to copy myPrewrittenBuff 

// Now lets read back the bitmap into another format...
BitmapData myReadingBuffer = new BitmapData();
ushort[] buff = new ushort[(3 * size.Width) * size.Height]; // ;Marshal.AllocHGlobal() if you want
GCHandle handle= GCHandle.Alloc(buff, GCHandleType.Pinned);
myReadingBuffer.Scan0 = Marshal.UnsafeAddrOfPinnedArrayElement(buff, 0);
myReadingBuffer.Height = size.Height;
myReadingBuffer.Width = size.Width;
myReadingBuffer.PixelFormat = PixelFormat.Format48bppRgb;
myReadingBuffer.Stride = 6 * size.Width;
// now read into that buff
BitmapData result = bitmap.LockBits(new Rectangle(0, 0, size.Width, size.Height), ImageLockMode.UserInputBuffer | ImageLockMode.ReadOnly, PixelFormat.Format48bppRgb, myReadingBuffer);
if (object.ReferenceEquals(result, myReadingBuffer)) {
    // Note: we pass here
    // and buff is filled
}
bitmap.UnlockBits(result);
handle.Free();
// use buff at will...

Si vous utilisez ILSpy, vous verrez que cette méthode est liée à GDI + et ces méthodes sont plus complètes.

Vous pouvez augmenter les performances en utilisant votre propre schéma de mémoire, mais sachez que Stride peut avoir besoin d'un certain alignement pour obtenir les meilleures performances.

Vous pourrez alors vous déchaîner par exemple en allouant énorme mémoire virtuelle mappée scan0 et les blit assez efficacement. Notez que l'épinglage d'un énorme tableau (et surtout de quelques-uns) ne sera pas un fardeau pour le GC et vous permettra de manipuler l'octet/court de manière totalement sûre (ou dangereux si vous recherchez la vitesse)

10
Boing

Je ne sais pas s'il y a une raison pour laquelle vous le faites comme vous êtes. Peut-être qu'il y en a. Il semble que vous soyez suffisamment hors des sentiers battus pour que vous puissiez essayer de faire quelque chose de plus avancé que ce que le titre de votre question implique ...

Cependant, la façon traditionnelle de créer un bitmap à partir d'un tableau d'octets est la suivante:

using (MemoryStream stream = new MemoryStream(byteArray))
{
     Bitmap bmp = new Bitmap(stream);
     // use bmp here....
}
6
Steve