Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Read and write directly to Unlocked Bitmap unmanaged memory (Scan0)

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;
            }
        }
    }
like image 857
Pedro77 Avatar asked Jun 10 '13 18:06

Pedro77


1 Answers

No, you cannot. The official explanation is clear about that.

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. The code writes the value 0xff00ff00 (green) to 1500 locations in the temporary buffer. Later, the call to Bitmap::UnlockBits copies those values to the Bitmap object itself.

I would agree that there is a "bug" in UnLockBits(), because every non ImageLockModeUserInputBuf BitmapData should have its field reset (especially scan0) after the 'release/unlock'.

Scan0 GDI managed buffers may still be accessible after UnLockBits, but this is pure luck you do not get a invalid memory reference hard fault. The graphic subsystem may need this memory space to backup another Bitmap, or the same Bitmap but for another rectangle or in another pixelformat.

Scan0 don't represent the internal data of the bitmap, but a COPY, writen to by GDI while LockBits(...| ImageLockModeRead...) and read from by GDI while UnLockBits() (.. if LockBitswith(.. | ImageLockModeWrite ..)

That is what BitmapData abstraction is. Now maybe if you use a rectangle equals to the bitmap size AND a pixelmode matching the one of your display card, GDI may return the actual pixel storage address of the bitmap into scan0 (and not a copy), but you should never rely on that (or make a program that only work on your own computer).

EDIT 1: I allready explained above why you are lucky to be able to use scan0 outside the lock. Because you use the original bmp PixelFormat and that GDI is optimized in that case to give you the pointer and not a copy. This pointer is valid until the OS will decide to free it. The only time there is a garantee is between LockBits and UnLockBits. Period. Here is the code to add to yours, put it in a Form to test it somewhat seriously. I can make it crash with a kind-of "neutral" call with Rotate180FlipX by hammering the button. The bitmap internals are private. Period. The OS may decide any moment to change its representation without even you making "action" on it (like minimizing the window, and zillions other possibilities).

EDIT 2: Your question:is there any practical difference locking a bitmap using ReadOnly or WriteOnly mode when no user buffer is given?

With or without user buffer, there IS a difference. One copy on LockBits(if readonly) AND/OR one copy on UnlockBits(if writeonly). Choose carefully to not do unwanted copies. Hint: stop thinking you are working in the same pixelformat, logically you do not. A write only buffer in 64bpp is received totally filled with noise (or untouched if it is also user buffer). You had better completely fill it before the unlock. (not just poking at some pixels). The naming of enum is misleading because WriteOnly | ReadOnly == ReadWrite

Accessing one pixel at a time using LockBits is BAD. Nobody wants to do that. What you do is to create/modify many*many pixel (using pointer/scan0) and commit them in quazy ATOMIC operation (Lock/Marhsal.Copy/UnLock) to the bitmap (and Invalidate()/redraw if you want to see something)

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));
    }
}
like image 114
Boing Avatar answered Sep 28 '22 16:09

Boing