Crear bitmap a partir de una matriz de bytes de datos de píxeles

Esta pregunta se trata de cómo leer / escribir, asignar y administrar los datos de píxeles de un bitmap.

Aquí hay un ejemplo de cómo asignar una matriz de bytes (memoria administrada) para datos de píxeles y crear un bitmap que lo usa:

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 

// Pero no siempre es verdad. Más información en 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; } 

Pensé que el bitmap haría una copia de los datos de la matriz, pero en realidad apunta a los mismos datos. ¿Podrías ver?

 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] 

Preguntas:

  1. ¿Es seguro crear un bitmap a partir de una matriz de bytes [] (memoria administrada) y libre () del GCHandle? Si no es seguro, tendré que mantener una matriz anclada, ¿qué tan malo es eso para GC / Performance?

  2. ¿Es seguro cambiar los datos (por ejemplo, datos [0] = 255;)?

  3. La dirección de un Scan0 puede ser cambiada por el GC? Quiero decir, obtengo el Scan0 de un bitmap bloqueado, luego lo deslocking y después de un tiempo lo vuelvo a bloquear, el Scan0 puede ser diferente.

  4. ¿Cuál es el propósito de ImageLockMode.UserInputBuffer en el método LockBits? ¡Es muy difícil encontrar información sobre eso! ¡MSDN no lo explica claramente!

EDICION 1: Algunos seguimiento

  1. Debes mantenerlo fijo. ¿Disminuirá la velocidad del GC? Lo he pedido aquí . Depende de la cantidad de imágenes y sus tamaños. Nadie me ha dado una respuesta cuantitativa. Parece que es difícil de determinar. También puede asignar la memoria usando Marshal o usar la memoria no administrada asignada por el bitmap.

  2. He hecho muchas pruebas usando dos hilos. Mientras el Bitmap esté bloqueado, está bien. Si el bitmap está desbloqueado, entonces no es seguro. Mi publicación relacionada sobre lectura / escritura directamente en Scan0 . La respuesta de Boing “Ya expliqué anteriormente por qué tiene suerte de poder usar scan0 fuera de la cerradura. Porque utiliza el pixelFormat bmp original y ese GDI está optimizado en ese caso para darle el puntero y no una copia. Este puntero es válido hasta que el sistema operativo decida liberarlo. La única vez que hay una garantía es entre LockBits y UnLockBits. Periodo “.

  3. Sí, puede suceder, pero las regiones de memoria grandes son tratadas de forma diferente por el GC, mueve / libera este objeto grande con menos frecuencia. Por lo tanto, GC puede tardar un tiempo en mover esta matriz. Desde MSDN : “Cualquier asignación mayor o igual a 85,000 bytes va en el large object heap (LOH) ” … “LOH solo se recostack durante una colección de generación 2”. .NET 4.5 tiene mejoras en LOH.

  4. Esta pregunta ha sido respondida por @Boing. Pero lo admitiré. No lo entendí del todo. Entonces, si Boing u otra persona pudieran please clarify it , me alegraría. Por cierto, ¿por qué no puedo leer / escribir directamente en Sca0 sin bloquearlo ? => No debe escribir directamente en Scan0 porque Scan0 apunta a una copia de los datos de bitmap realizados por la memoria no administrada (dentro de GDI). Después del deslocking, esta memoria se puede reasignar a otras cosas, ya no es seguro que Scan0 apunte a los datos de bitmap reales. Esto se puede reproducir haciendo que el Scan0 se bloquee, se desbloquee y se gire un poco en el bitmap desbloqueado. Después de algún tiempo, Scan0 apuntará a una región no válida y obtendrá una excepción cuando intente leer / escribir en su ubicación de memoria.

  1. Es seguro si marshal.copy datos en lugar de configuración scan0 (directamente o a través de esa sobrecarga de BitMap ()). No desea mantener los objetos administrados inmovilizados, esto limitará el recolector de elementos no utilizados.
  2. Si copias, perfectamente seguro.
  3. La matriz de entrada se administra y puede ser movida por el GC, scan0 es un puntero no administrado que se desactualizaría si la matriz se moviera. El objeto Bitmap en sí mismo se administra pero establece el puntero scan0 en Windows mediante un identificador.
  4. ImageLockMode.UserInputBuffer es? Aparentemente se puede pasar a LockBits, tal vez le dice a Bitmap () que copie los datos de la matriz de entrada.

Código de ejemplo para crear un bitmap en escala de grises de una matriz:

  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, eg with a for loop over an input array Marshal.Copy(rgbValues, 0, ptr, bytes); b.UnlockBits(bmpData); return b; 

Con respecto a su pregunta 4: ImageLockMode.UserInputBuffer puede darle el control del proceso de asignación de esa gran cantidad de memoria que podría referenciarse en un objeto BitmapData .

Si elige crear usted mismo el objeto BitmapData , puede evitar un Marshall.Copy . Luego tendrá que usar esta bandera en combinación con otra ImageLockMode .

Tenga en cuenta que es un negocio complicado, especialmente con respecto a Stride y PixelFormat.

Aquí hay un ejemplo que obtendría de una sola vez el contenido del búfer de 24bbp en un BitMap y luego, en otro plano, lo leería en otro búfer 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 usa ILSpy, verá que este método de enlace a GDI + y esos métodos de ayuda son más completos.

Puede boost el rendimiento utilizando su propio esquema de memoria, pero tenga en cuenta que Stride puede necesitar cierta alineación para obtener el mejor rendimiento.

Entonces podrá volverse loco, por ejemplo, asignando una enorme memoria virtual mapeada scan0 y blitándolas de manera bastante eficiente. Tenga en cuenta que fijar un gran conjunto (y especialmente algunos) no será una carga para el GC y le permitirá manipular el byte / corto de una manera totalmente segura (o inseguro si busca velocidad)

No estoy seguro de si hay una razón por la que lo haces como eres. Tal vez hay Parece que estás fuera de los caminos trillados lo suficiente como para que intentes hacer algo más avanzado de lo que implica el título de tu pregunta …

Sin embargo, la forma tradicional de crear un bitmap a partir de una matriz de bytes es:

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

Aquí hay un código de muestra que escribí para convertir una matriz de píxeles de bytes a una imagen de escala de grises de 8 bits (bmp). Este método acepta la matriz de píxeles, el ancho de la imagen y la altura como argumentos //

 public Bitmap Convert2Bitmap(byte[] DATA, int width, int height) { Bitmap Bm = new Bitmap(width,height,PixelFormat.Format24bppRgb); 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; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int Value = DATA[x + (y * width)]; Color C = ncp.Entries[Value]; Bm.SetPixel(x,y,C); } } return Bm; }