直接读取和写入Unlocked Bitmap非托管内存(Scan0)

从解锁的Bitmap非托管内存直接写入和读取是否可以?

在我解锁Bitmap后,我可以继续使用BitmapData吗? 我做了一个测试应用程序,我可以在鼠标位置读取PictureBox的Bitmap像素,而另一个线程正在将像素写入同一个Bitmap。

编辑1:正如Boing在他的回答中指出的那样:“Scan0没有指向Bitmap对象的实际像素数据;而是指向一个临时缓冲区,它表示Bitmap对象中像素数据的一部分。” 来自MSDN 。

但是一旦我获得Scan0,我就能够读取/写入位图而无需Lockbits或UnlockBits! 我在一个post里做了很多次。 相对于MSDN,它不应该发生,因为Scan0指向位图数据的COPY! 好吧,在C#中,所有测试都显示它不是副本。 在C ++中,我不知道它是否正常工作。

编辑2:使用旋转方法有时会使操作系统释放位图像素数据副本。 结论, it is not safe to read/write an unlocked Bitmap Scan0 。 谢谢Boing的回答和评论!

下面是我如何获取BitmapData并读取和写入像素值。

  ///  /// Locks and unlocks the Bitmap to get the BitmapData. ///  /// Bitmap /// BitmapData public static BitmapData GetBitmapData(Bitmap bmp) { BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat); bmp.UnlockBits(bmpData); return bmpData; } ///  /// Get pixel directly from unamanged pixel data based on the Scan0 pointer. ///  /// BitmapData of the Bitmap to get the pixel /// Pixel position /// Channel /// Pixel value public static byte GetPixel(BitmapData bmpData, Point p, int channel) { if ((pX > bmpData.Width - 1) || (pY > bmpData.Height - 1)) throw new ArgumentException("GetPixel Point p is outside image bounds!"); int bitsPerPixel = ((int)bmpData.PixelFormat >> 8) & 0xFF; int bpp = bitsPerPixel / 8; byte data; int id = pY * bmpData.Stride + pX * bpp; unsafe { byte* pData = (byte*)bmpData.Scan0; data = pData[id + channel]; } return data; } //Non UI Thread private void DrawtoBitmapLoop() { while (_drawBitmap) { _drawPoint = new Point(_drawPoint.X + 10, _drawPoint.Y + 10); if (_drawPoint.X > _backImageData.Width - 20) _drawPoint.X = 0; if (_drawPoint.Y > _backImageData.Height - 20) _drawPoint.Y = 0; DrawToScan0(_backImageData, _drawPoint, 1); Thread.Sleep(10); } } private static void DrawToScan0(BitmapData bmpData, Point start, int channel = 0) { int x = start.X; int y = start.Y; int bitsPerPixel = ((int)bmpData.PixelFormat >> 8) & 0xFF; int bpp = bitsPerPixel / 8; for (int i = 0; i < 10; i++) { unsafe { byte* p = (byte*)bmpData.Scan0; int id = bmpData.Stride * y + channel + (x + i) * bpp; p[id] = 255; } } } 

你不能。 官方解释很清楚。

Scan0 指向Bitmap对象的实际像素数据; 相反,它指向一个临时缓冲区,表示Bitmap对象中的一部分像素数据。 代码将值0xff00ff00(绿色)写入临时缓冲区中的1500个位置。 稍后,对Bitmap :: UnlockBits的调用会将这些值复制到Bitmap对象本身。

我同意UnLockBits()中存在“bug”,因为每个非ImageLockModeUserInputBuf BitmapData应该在’release / unlock’之后重置其字段(特别是scan0)。

UnLockBits之后,仍然可以访问Scan0 GDI托管缓冲区,但这是纯粹的运气,您不会得到无效的内存引用硬故障。 图形子系统可能需要此存储空间来备份另一个位图,或相同的位图,但另一个矩形或另一个像素格式。

Scan0不代表位图的内部数据,而是一个COPY ,由GDI LockBits(...| ImageLockModeRead...)并由GDI读取而UnLockBits() (.. if LockBitswith(.. | ImageLockModeWrite ..)

这就是BitmapData的抽象。 现在也许如果你使用一个等于位图大小的矩形和一个匹配你的显卡的像素模式,GDI 可能会将位图的实际像素存储地址返回到scan0(而不是副本),但你永远不应该依赖它(或制作仅在您自己的计算机上工作的程序)。

编辑1:我已经在上面解释了为什么你很幸运能够在锁外使用scan0。 因为您使用原始的 bmp PixelFormat并且在这种情况下优化了GDI,以便为您提供指针而不是副本。 该指针有效,直到操作系统决定释放它为止。 唯一一次有保证是 LockBitsUnLockBits 之间 。 期。 这是添加到您的代码,将其放在一个表单中,以便对它进行一些严肃的测试。 我可以通过敲击按钮使用Rotate180FlipX进行一种“中性”调用来使其崩溃。 位图内部是私有的 。 期。 操作系统可以决定改变其表示的任何时刻,即使你对其进行“动作”(例如最小化窗口,以及其它可能性)。

编辑2:您的问题: 当没有给出用户缓冲区时,使用ReadOnly或WriteOnly模式锁定位图有什么实际区别吗?

无论有没有用户缓冲,都有区别。 LockBits上的一个副本(如果只读) 和/或 UnlockBits上的一个副本(如果只是readonly)。 仔细选择不要做不需要的副本。 提示:不要以为你是在使用相同的像素格式,逻辑上你没有 。 接收到64bpp的写缓冲区完全充满噪声 (如果它也是用户缓冲区,则不接触)。 在解锁之前你最好完全填满它。 (不只是戳一些像素)。 枚举的命名具有误导性,因为WriteOnly | ReadOnly == ReadWrite

使用LockBits一次访问一个像素是不好的。 没有人愿意这样做。 你要做的是创建/修改许多*许多像素(使用指针/ scan0)并将它们在quazy ATOMIC操作(Lock / Marhsal.Copy / UnLock)中提交到位图(如果你想看到某些内容,则将它们提交给Invalidate()/ redraw )

 public MainForm() { InitializeComponent(); pictureBox.SizeMode = PictureBoxSizeMode.StretchImage; // use a .gif for 8bpp Bitmap bmp = (Bitmap)Bitmap.FromFile(@"C:\Users\Public\Pictures\Sample Pictures\Forest Flowers.jpg"); pictureBox.Image = bmp; _backImageData = GetBitmapData(bmp); _drawBitmap = true; _thread= new Thread(DrawtoBitmapLoop); _thread.IsBackground= true; _thread.Start(); button.Text = "Let's get real"; button.Click += (object sender, EventArgs e) => { // OK on my system, it does not rreallocate but ... bmp.RotateFlip(RotateFlipType.Rotate180FlipX); // ** FAIL with Rotate180FlipY on my system** }; } Thread _thread; bool _drawBitmap; BitmapData _backImageData; //Non UI Thread private void DrawtoBitmapLoop() { while (_drawBitmap) { ScrollColors(_backImageData); this.Invoke((ThreadStart)(() => { if (!this.IsDisposed) this.pictureBox.Invalidate(); })); Thread.Sleep(100); } } private unsafe static void ScrollColors(BitmapData bmpData) { byte* ptr = (byte*)bmpData.Scan0; ptr--; byte* last = &ptr[(bmpData.Stride) * bmpData.Height]; while (++ptr <= last) { *ptr = (byte)((*ptr << 7) | (*ptr >> 1)); } }