从像素数据的字节数组创build位图
这个问题是关于如何读/写,分配和pipe理位图的像素数据。
以下是如何为像素数据分配字节数组(pipe理内存)并使用它创build位图的示例:
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
//但这并不总是如此。 更多信息在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]
问题:
-
从byte []数组(托pipe内存)和free()GCHandle创build位图是否安全? 如果不安全,我需要保留一个固定的数组,这对GC /性能有多糟糕?
-
是否安全地更改数据(例如:data [0] = 255;)?
-
Scan0的地址可以通过GC更改? 我的意思是,我从一个locking的位图中获取Scan0,然后解锁它,并在一段时间再次locking之后,Scan0可以不同?
-
LockBits方法中ImageLockMode.UserInputBuffer的用途是什么? 很难find有关的信息! MSDN不解释清楚!
编辑1:一些后续
-
你需要保持固定。 它会减慢GC吗? 我在这里问过 这取决于图像的数量和大小。 没有人给我一个量化的答案。 它接缝很难确定。 你也可以使用Marshal来分配内存,或者使用由Bitmap分配的非托pipe内存。
-
我用两个线程做了很多testing。 只要位图被locking,就可以了。 如果位图解锁,比不安全! 我的相关文章直接读/写到Scan0 。 Boing的回答“我已经在上面解释了为什么你很幸运能够在锁外使用scan0,因为你使用了原始的bmp PixelFormat,并且GDI在这种情况下被优化,给你指针而不是副本,这个指针是有效的直到操作系统决定释放它为止。唯一有保证的时间是在LockBits和UnLockBits之间。
-
是的,它可能会发生,但是大的内存区域被GC处理为不同的,它移动/释放这个较大的对象的频率较低。 所以GC移动这个数组可能需要一些时间。 从MSDN :“任何大于或等于
85,000 bytes
分配在large object heap (LOH)
”…“LOH仅在第2代收集期间收集”。 .NET 4.5在LOH方面有所改进。 -
这个问题已经被@Boing回答了。 但我会承认。 我没有完全理解它。 所以如果
Boing
或其他人可以please clarify it
,我会很高兴。 顺便说一下,为什么我不能直接读/写Sca0而不locking ? =>您不应直接写入Scan0,因为Scan0指向由非托pipe内存(在GDI内部)制作的位图数据的副本。 解锁后,这个内存可以重新分配给其他的东西,它不再确定Scan0将指向实际的位图数据。 这可以被重现获取Scan0在locking,解锁,并在解锁的位图做一些旋转。 一段时间后,Scan0将指向一个无效的区域,当你尝试读/写它的内存位置时,你将会遇到一个exception。
- 它是安全的,如果你marshal.copy数据,而不是设置scan0(直接或通过BitMap()的重载)。 你不想保留托pipe对象,这将约束垃圾收集器。
- 如果你复制,完全安全。
- input数组是被pipe理的,并且可以被GC移动,scan0是一个非托pipe的指针,如果数组移动,它将会过期。 位图对象本身被pipe理,但是通过一个句柄在Windows中设置scan0指针。
- ImageLockMode.UserInputBuffer是? 显然它可以传递给LockBits,也许它告诉Bitmap()复制input数组数据。
用于从数组创build灰度位图的示例代码:
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;
关于你的问题4: ImageLockMode.UserInputBuffer
可以让你控制那些可以被引用到BitmapData
对象中的大量内存的分配过程。
如果你select创build自己的BitmapData
对象,你可以避免Marshall.Copy
。 您将不得不与另一个ImageLockMode
使用此标志。
请注意,这是一个复杂的业务,特别是有关Stride和PixelFormat。
下面是一个例子,将一个 24bpp缓冲区的内容放到一个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 + ,这些方法帮助更完整。
您可以使用自己的内存scheme来提高性能,但请注意,Stride可能需要进行一些调整才能获得最佳性能。
然后,你将能够疯狂地分配巨大的虚拟内存映射scan0和相当有效地blit他们。 请注意,固定巨大的数组(特别是less数)不会成为GC的负担,并且允许您以完全安全的方式操纵字节/短(如果您寻求速度,则不安全)
我不确定是否有理由按照自己的方式进行操作。 也许有。 看起来你已经不够好了,所以你可能会试图做比你的问题的标题更高级的东西。
但是,从字节数组创build位图的传统方法是:
using (MemoryStream stream = new MemoryStream(byteArray)) { Bitmap bmp = new Bitmap(stream); // use bmp here.... }
这是我写的一个示例代码,将像素的字节数组转换为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; }