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

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

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

我以为该位图可以复制数组数据,但实际上它指向相同的数据。您可以看到:
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 []数组(托管内存)和free()GCHandle创建位图是否安全?如果不安全,则需要保留固定的阵列,这对GC/Performance有多严重?
  • 更改数据是否安全(例如:data [0] = 255;)?
  • GC可以更改Scan0的地址吗?我的意思是,我从锁定的位图获取Scan0,然后将其解锁,再过一段时间锁定后,Scan0会有所不同吗?
  • LockBits方法中ImageLockMode.UserInputBuffer的用途是什么?很难找到有关该信息! MSDN没有清楚地解释它!

  • 编辑1:一些后续
  • 您需要保持固定。会降低GC的速度吗? I've asked it here。这取决于图像的数量及其大小。没有人给我定量的答案。它似乎很难确定。
    您也可以使用元帅来分配内存,也可以使用位图分配的非托管内存。
  • 我已经使用两个线程做了很多测试。只要位图被锁定就可以。如果位图是解锁的,那将是不安全的! My related post about read/write directly to Scan0。 Boing的回答“我已经在上面解释了为什么您很幸运能够在锁之外使用scan0。因为您使用的是原始bmp PixelFormat,并且在这种情况下对GDI进行了优化,可以为您提供指针而不是副本。此指针有效直到操作系统决定释放它为止。唯一可以保证的时间是在LockBits和UnLockBits之间。
  • 是的,可能会发生,但是GC对大型内存区域的处理方式有所不同,它不太频繁地移动/释放此大型对象。因此,GC可能需要一段时间才能移动该阵列。 From MSDN:“任何大于或等于85,000 bytes的分配都将在large object heap (LOH)上进行……”“仅在第二代收集期间收集LOH”。 .NET 4.5的LOH有所改进。
  • @Boing已回答了这个问题。但是我要承认。我没有完全理解它。因此,如果Boing或其他人可以使用please clarify it,我将很高兴。顺便说一句,为什么我不能只是directly read/write to Sca0 without locking? =>您不应直接写入Scan0,因为Scan0指向非托管内存(在GDI内部)生成的位图数据的副本。解锁后,该内存可以重新分配给其他内容,不再确定Scan0会指向实际的位图数据。可以将Scan0锁定,解锁并在解锁的位图中进行一些旋转平移,以重现这一点。一段时间后,Scan0将指向无效的区域,并且尝试读取/写入其内存位置时会遇到异常。
  • 最佳答案

  • 如果封送数据而不是设置scan0(直接或通过BitMap()的重载)进行封送,则它是安全的。您不想让托管对象保持固定状态,这将限制垃圾收集器。
  • 如果您进行复制,绝对安全。
  • 输入数组是受管理的,可以由GC进行移动,scan0是非托管指针,如果移动了数组,该指针将过时。 Bitmap对象本身是受管理的,但通过句柄在Windows中设置scan0指针。
  • ImageLockMode.UserInputBuffer是吗?显然,它可以传递给LockBits,也许它告诉Bitmap()复制输入数组数据。

  • 从数组创建灰度位图的示例代码:
        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;
    

    09-25 16:06