【问题标题】:Read and write directly to Unlocked Bitmap unmanaged memory (Scan0)直接读写 Unlocked Bitmap 非托管内存 (Scan0)
【发布时间】:2013-06-10 18:17:14
【问题描述】:

可以直接从解锁的位图非托管内存写入和读取吗?

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

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

但是一旦我得到 Scan0,我就可以在不需要 Lockbits 或 UnlockBits 的情况下读取/写入 Bitmap!我在一个线程中做了很多次。根据 MSDN,它不应该发生,因为 Scan0 指向位图数据的副本!好吧,在 C# 中,所有测试都表明它不是副本。在 C++ 中,我不知道它是否可以正常工作。

编辑 2: 有时使用旋转方法会使操作系统释放位图像素数据副本。结论,@ 987654323@。感谢 Boing 的回答和 cmets!

以下是我如何获取 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;
            }
        }
    }

【问题讨论】:

  • 对于 c# 中的大图像处理,是的,这是获取和设置像素的一种非常快速的方法,但是如果您正在处理小图像 (GetPixel 和SetPixel 会容易很多
  • @Mehran,请看一下我的编辑和 Boings 的回答。您是否注意到我正在读取/写入未锁定的位图?因此,对于 MSDN,这应该行不通。
  • 我尝试了你所说的,但没有解锁位,我在picturebox 中得到一个白色背景的红十字,并且没有图像! ,正如您已经提到的那样,这不应该起作用,但我不知道为什么在您的情况下它会起作用!
  • @Mehran,你看过方法“public static BitmapData GetBitmapData(Bitmap bmp)”吗?好吧,正如你所见,我解锁了它。不解锁我会像你一样得到一个红十字会。 1. 创建位图。 2. 获取位图数据。 3. 将位图添加到 PictureBox 4. 对位图进行任何操作(读/写),结果将显示在 PictureBox 中。 :)

标签: c# bitmap lockbits unmanaged-memory


【解决方案1】:

不,你不能。 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 ..)时由GDI读取

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

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

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

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

使用 LockBits 一次访问一个像素是不好的。没有人愿意这样做。你要做的是创建/修改 ma​​ny*many 像素(使用指针/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));
    }
}

【讨论】:

  • 嗯,这篇文章显然支持你所说的。 “Scan0 并不指向 Bitmap 对象的实际像素数据;相反,它指向一个临时缓冲区,该缓冲区表示 Bitmap 对象中的一部分像素数据。”但是当我写入 Scan0 时,显示的位图会发生变化,但文章说它应该只发生在 UnLockBits 之后!也许在 C++ 中可以正常工作。有人可以试试这个吗?你是说我很幸运能对实际的位图数据进行 Scan0,但我一直都很“幸运”,在不止一台机器上。我想了解实际发生的情况。
  • @Pedro77 这不是 C++ 问题。 .NET 直接将调用转发到 OS/C++ GDI 方法(使用 ILSpy)。它在这两种情况下都应该有效。见编辑。当请求的 PixelFormat 相同时,不需要复制/转换。
  • “GDI 在这种情况下进行了优化,以便为您提供指针而不是副本”在哪里说在我的回复中说。它是通过代码体验的,它是完全合乎逻辑的,因为指针是其自身的副本。错误是当客户端请求只读时,操作系统不会在写访问时锁定内存(使用 MMU)。下一个操作系统版本或补丁可能会改变它(它不会所以不要惊慌)。 停止在LockBits之外使用Scan0,那没有用。或者只是希望最坏的情况。
  • 如果您以各种可能的方式创建位图(使用 Scan0 或 UserInputBuffer LockBits 写入的ctor),结果是相同的 :位图拥有自己的副本,不会像您一样触及您的副本不应该碰他们的
  • 我很高兴直接使用 Scan0。但是你已经告诉我这是错误的。非常感谢波音。另一个问题:当没有给定用户缓冲区时,使用 ReadOnly 或 WriteOnly 模式锁定位图有什么实际区别吗?
猜你喜欢
  • 2021-07-04
  • 1970-01-01
  • 2012-01-30
  • 1970-01-01
  • 1970-01-01
  • 2020-07-11
  • 1970-01-01
  • 2013-03-14
  • 1970-01-01
相关资源
最近更新 更多