从像素数据的字节数组创建位图

时间:2011-07-21 20:40:56

标签: c# bitmap bitmapdata

这个问题是关于如何读/写,分配和管理位图的像素数据。

以下是如何为像素数据分配字节数组(托管内存)并使用它创建位图的示例:

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

//但并非总是如此。 More info at 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;
}

我认为Bitmap会复制数组数据,但它实际上指的是相同的数据。你能看到:

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]

问题:

  1. 从byte []数组(托管内存)和free()GCHandle创建位图是否安全?如果它不安全,我需要保留一个固定阵列,这对GC / Performance有多糟糕?

  2. 更改数据是否安全(例如:data [0] = 255;)?

  3. 可以通过GC更改Scan0的地址吗?我的意思是,我从一个锁定的位图中获取Scan0,然后将其解锁,经过一段时间再锁定后,Scan0会有所不同吗?

  4. LockBits方法中ImageLockMode.UserInputBuffer的用途是什么?很难找到相关的信息! MSDN没有明确解释它!

  5. 编辑1:一些跟进

    1. 你需要保持固定。它会降低GC的速度吗? I've asked it here。这取决于图像的数量及其大小。没有人给我一个定量答案。它接缝很难确定。 您也可以使用Marshal分配内存或使用Bitmap分配的非托管内存。

    2. 我使用两个线程做了很多测试。只要位图被锁定就可以了。如果位图解锁,则不安全! My related post about read/write directly to Scan0。 Boing的回答“我已经在上面解释了为什么你能够在锁外使用scan0是幸运的。因为你使用原始的bmp PixelFormat并且在这种情况下GDI被优化以给你指针而不是副本。这个指针是有效的直到操作系统决定释放它。唯一有保证的时间是LockBits和UnLockBits。期间。“

    3. 是的,它可能会发生,但是大的内存区域被GC处理不同,它会更频繁地移动/释放这个大对象。因此GC移动此阵列可能需要一段时间。 From MSDN:“任何大于或等于85,000 bytes的分配都在large object heap (LOH)上......”“LOH仅在第2代收集时收集”。 .NET 4.5在LOH方面有所改进。

    4. @Boing已经回答了这个问题。但我会承认。我没有完全理解它。因此,如果Boing或其他人可以please clarify it,我会很高兴。顺便说一下,为什么我不能只directly read/write to Sca0 without locking? =>您不应直接写入Scan0,因为Scan0指向由非托管内存(在GDI内部)生成的位图数据的副本。解锁后,这个内存可以重新分配给其他东西,它不再确定Scan0将指向实际的位图数据。这可以重现,让Scan0处于锁定状态,解锁,并在解锁的位图中进行一些旋转。一段时间后,Scan0将指向一个无效区域,当您尝试读取/写入其内存位置时,您将收到异常。

4 个答案:

答案 0 :(得分:24)

  1. 如果你编组复制数据而不是设置scan0(直接或通过BitMap()的重载),它是安全的。您不希望将托管对象固定,这将限制垃圾收集器。
  2. 如果你复制,非常安全。
  3. 输入数组是受管理的,可以由GC移动,scan0是一个非托管指针,如果数组移动,它将过时。 Bitmap对象本身是受管理的,但是通过句柄在Windows中设置scan0指针。
  4. ImageLockMode.UserInputBuffer是?显然它可以传递给LockBits,也许它告诉Bitmap()复制输入数组数据。
  5. 从数组创建灰度位图的示例代码:

        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;
    

答案 1 :(得分:10)

关于您的问题4: ImageLockMode.UserInputBuffer可以让您控制可以引用到BitmapData对象的大量内存的分配过程。

如果您选择自己创建BitmapData对象,则可以避免使用Marshall.Copy。然后,您必须与另一个ImageLockMode组合使用此标记。

  

请注意这是一项复杂的业务,特别是关于Stride   和PixelFormat。

这是一个例子,可以一次性将24bbp缓冲区的内容放到BitMap上,然后在另一个镜头中将其读回另一个缓冲区到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...

如果您使用ILSpy,您会看到此方法链接到GDI+,这些方法有助于更完整。

  

您可以使用自己的内存方案来提高性能,但是   请注意,Stride可能需要进行一些调整以获得最佳效果   性能

然后你可以疯狂地例如分配巨大的虚拟内存映射scan0并非常有效地blit它们。 请注意,固定大型阵列(尤其是少数阵列)不会对GC造成负担,并且允许您以完全安全的方式操作字节/短路(或者如果您寻求速度则不安全)

答案 2 :(得分:5)

我不确定你是否有理由这样做。也许有。看起来你已经走得太远了,所以你可能会尝试做一些比问题标题所暗示的更先进的事情......

但是,从字节数组创建位图的传统方法是:

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

答案 3 :(得分:-2)

这是我编写的用于将字节数组像素转换为8位灰度图像(bmp)的示例代码 此方法接受像素数组,图像宽度和高度作为参数 //

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;
}