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

时间:2013-06-10 18:17:14

标签: c# bitmap lockbits unmanaged-memory

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

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

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

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

编辑2:使用rotate方法有时会使操作系统释放位图像素数据副本。结论,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;
            }
        }
    }

1 个答案:

答案 0 :(得分:7)

不,你不能。 official explanation很清楚这一点。

  

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

我同意UnLockBits()中存在“错误”,因为每个非ImageLockModeUserInputBuf BitmapData都应在“释放/解锁”后重置其字段(特别是scan0)。

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

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

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

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

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

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

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

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