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

Ped*_*o77 6 c# bitmap lockbits unmanaged-memory

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

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

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

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

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

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

    /// <summary>
    /// Locks and unlocks the Bitmap to get the BitmapData.
    /// </summary>
    /// <param name="bmp">Bitmap</param>
    /// <returns>BitmapData</returns>
    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;
    }

    /// <summary>
    /// Get pixel directly from unamanged pixel data based on the Scan0 pointer.
    /// </summary>
    /// <param name="bmpData">BitmapData of the Bitmap to get the pixel</param>
    /// <param name="p">Pixel position</param>
    /// <param name="channel">Channel</param>
    /// <returns>Pixel value</returns>
    public static byte GetPixel(BitmapData bmpData, Point p, int channel)
    {
        if ((p.X > bmpData.Width - 1) || (p.Y > 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 = p.Y * bmpData.Stride + p.X * 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;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

Boi*_*ing 7

你不能.在官方的解释是清楚这一点.

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

我同意存在"错误" UnLockBits(),因为每个非ImageLockModeUserInputBufBitmapData应该在'release/unlock'之后重置其字段(特别是scan0).

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

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

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

编辑1:我已经在上面解释了为什么你能幸运能够在锁外使用scan0.因为你使用原始的 bmp PixelFormat并且在这种情况下优化了GDI以给你指针而不是副本.该指针有效,直到操作系统决定释放它为止.在唯一一次有一个garantee是之间 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));
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @ Pedro77这不是C++问题..NET转发直接调用OS/C++ GDI方法(使用ILSpy).它在两种情况下都有效.见编辑.当PixelFormat请求相同时,不需要复制/转换. (3认同)