6

Is that ok to Write and Read directly from a unlocked Bitmap unmanaged memory?

Can I keep using the BitmapData after I UnlockBits of the Bitmap? I did a test app where I can read the pixel of the Bitmap of a PictureBox at mouse position while another thread is writing pixels to the same Bitmap.

EDIT 1: As Boing have pointed out in his answer: "Scan0 does not point to the actual pixel data of the Bitmap object; rather, it points to a temporary buffer that represents a portion of the pixel data in the Bitmap object." from MSDN.

But once I get the Scan0, I'm able to read/write to the Bitmap without the need of Lockbits or UnlockBits! I'm doing this a lot of times in a thread. Accordingly to MSDN, it should not happen, because Scan0 points to a COPY of the Bitmap data! Well, in C# all the test shows that it is not a copy. In C++ I don't know if it works as it should.

EDIT 2: Using the rotate method some times makes the OS to free the Bitmap pixel data copy. Conclusion, it is not safe to read/write an unlocked Bitmap Scan0. Thanks Boing for your answer and comments!

Below is how I get the BitmapData and read and write the pixel value.

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

1 回答 1

7

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

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

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

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

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

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

编辑1:我已经在上面解释了为什么你很幸运能够在锁外使用scan0。因为您使用原始bmpPixelFormat并且 GDI 在这种情况下进行了优化,以便为您提供指针而不是副本。这个指针在操作系统决定释放它之前是有效的。唯一有保证的时间是和之间 。时期。这是要添加到您的代码,将其放入表单中以进行一些认真的测试。我可以通过敲击按钮 使其以一种“中性”呼叫崩溃。位图内部是私有的LockBitsUnLockBitsRotate180FlipX. 时期。操作系统可以随时决定更改其表示,甚至您无需对其进行“操作”(例如最小化窗口,以及无数其他可能性)。

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

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

使用 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));
    }
}
于 2013-06-18T11:16:50.067 回答