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

1

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;
            }
        }
    }
Crocus answered 10/6, 2013 at 18:17 Comment(4)
for large image processing in c# , yes , this is a very fast way for getting and setting pixels , but if you're working with small images ( < 500*500) , using GetPixel and SetPixel would be a lot easierArequipa
@Mehran, please take a look at my edit and Boings answer. Have you noticed that I'm reading/writing to a unlocked Bitmap? Accordingly to MSDN this should not work.Crocus
I tried what you said , but without unlocking bits I get a red cross in white background in my picturebox and no image ! , As you already have mentioned this should not work , but I have no idea why it does in your case !Arequipa
@Mehran, Have you looked the method "public static BitmapData GetBitmapData(Bitmap bmp)"? Well, as you can see, I UNLOCK it. Without unlocking I'll get a red-cross just like you. 1. Create Bitmap. 2. Get BitmapData. 3. Add Bitmap to PictureBox 4. Do whatever you want with the Bitmap (read/write) the result will be displayed at the PictureBox. :)Crocus
M
8

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));
    }
}
Mammal answered 18/6, 2013 at 11:16 Comment(10)
Well, the article clearly supports what you are saying. "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." But when I write to Scan0, the displayed Bitmap changes, but the article says that it should only happen after a UnLockBits! Maybe in C++ is works as it should. Can someone try this? You are saying I'm lucky to have a Scan0 to the actual Bitmap data, but I'm been "lucky" ALL the time, at more than one machine. I want to understand what is actually happening.Crocus
@Crocus This is not a C++ issue. .NET forward call directly to OS/C++ GDI method (use ILSpy). It works has it should in both case. See edit. A copy/convertion is not required when PixelFormat requested is identical.Mammal
"GDI is optimized in that case to give you the pointer and not a copy" where this is said? You are right, the roteflip makes the Scan0 changes some times! What if I create a Bitmap using my unmanaged buffer? I think in this case the Scan0 wount change. I'm going to test it.Crocus
If I create a Bitmap with my per-allocated unmanaged data, the OS still moves the data at the LockBits.Crocus
"GDI is optimized in that case to give you the pointer and not a copy" where this is said? It is said in my response. It is experienced trough the code, it is totaly logical because a pointer IS a copy of itself. The bug is that the OS won't lock the memory on write access (using the MMU) when client request read-only. Next OS version or patch may change it (it won't so don't panic). Stop using Scan0 outside LockBits, there is NO use for THAT. Or just hope for the worst.Mammal
If you create Bitmap in every way possible (ctor with Scan0 or UserInputBuffer LockBits write), the result is the same : Bitmaps own their own copies, an will NOT touch yours like you should NOT touch theirs.Mammal
I was so happy using the Scan0 directly. But you have showed me that it is wrong. Thank you very much Boing. Another question: is there any practical difference locking a bitmap using ReadOnly or WriteOnly mode when no user buffer is given?Crocus
@Crocus Your are welcomed :-). I have edited my answer to respond to this next question.Mammal
For those of you coming from the .NET docs, BitmapData.Scan0 on MSDN says says Gets or sets the address of the first pixel data in the bitmap. This can also be thought of as the first scan line in the bitmap. but Boing is right, so the .NET docs are a little deceptive at best. The GDI+ function BitmapData.Scan0 says Scan0 does not point to the actual pixel data of the Bitmap objectHajji
.. for those that are interested, you can see that BitmapData.Scan0 really does just call the GDI+ function here in the reference source.Hajji

© 2022 - 2024 — McMap. All rights reserved.