WPF CreateBitmapSourceFromHBitmap() memory leak
Asked Answered
B

6

53

I need to draw an image pixel by pixel and display it inside a WPF. I am attempting to do this by using a System.Drawing.Bitmap then using CreateBitmapSourceFromHBitmap() to create a BitmapSource for a WPF Image control. I have a memory leak somewhere because when the CreateBitmapSourceFromBitmap() is called repeatedly the memory usage goes up and does not drop off until the application is ended. If I don't call CreateBitmapSourceFromBitmap() there is no noticeable change in memory usage.

for (int i = 0; i < 100; i++)
{
    var bmp = new System.Drawing.Bitmap(1000, 1000);
    var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
        bmp.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty,
        System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
    source = null;
    bmp.Dispose();
    bmp = null;
}

What can I do to free the BitmapSource memory?

Bunch answered 9/10, 2009 at 21:17 Comment(0)
C
89

MSDN for Bitmap.GetHbitmap() states:

Remarks

You are responsible for calling the GDI DeleteObject method to free the memory used by the GDI bitmap object.

So use the following code:

// at class level
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);

// your code
using (System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(1000, 1000)) 
{
    IntPtr hBitmap = bmp.GetHbitmap(); 

    try 
    {
        var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
    }
    finally 
    {
        DeleteObject(hBitmap);
    }
}

I also replaced your Dispose() call by an using statement.

Changeling answered 9/10, 2009 at 21:26 Comment(2)
That works. There is a bit of residual memory held after the test, but the garbage collector picks it up. Thanks Julien.Bunch
You are awesome. I've been trying to eliminate this bug for a long time, and your solution worked like a charm. thanksEyrir
V
24

Whenever dealing with unmanaged handles it can be a good idea to use the "safe handle" wrappers:

public class SafeHBitmapHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    [SecurityCritical]
    public SafeHBitmapHandle(IntPtr preexistingHandle, bool ownsHandle)
        : base(ownsHandle)
    {
        SetHandle(preexistingHandle);
    }

    protected override bool ReleaseHandle()
    {
        return GdiNative.DeleteObject(handle) > 0;
    }
}

Construct one like so as soon as you surface a handle (ideally your APIs would never expose IntPtr, they would always return safe handles):

IntPtr hbitmap = bitmap.GetHbitmap();
var handle = new SafeHBitmapHandle(hbitmap , true);

And use it like so:

using (handle)
{
  ... Imaging.CreateBitmapSourceFromHBitmap(handle.DangerousGetHandle(), ...)
}

The SafeHandle base gives you an automatic disposable/finalizer pattern, all you need to do is override the ReleaseHandle method.

Vitamin answered 12/8, 2011 at 2:30 Comment(2)
Very nice mini-article about something I should know better.Granulate
the "answer" pointed to the right direction but still did not work - I still got out of memory - but your solution works flawlessly - and not only that but also I love wrapping in this manner - it's true abstraction and the future of coding - sorry got carried awayAmnesia
Q
5

I had the same requirement and issue (memory leak). I implemented the same solution as marked as answer. But although the solution works, it caused an unacceptable hit to performance. Running on a i7, my test app saw a steady 30-40% CPU, 200-400MB RAM increases and the garbage collector was running almost every millisecond.

Since I'm doing video processing, I'm in need of much better performance. I came up with the following, so thought I would share.

Reusable Global Objects

//set up your Bitmap and WritableBitmap as you see fit
Bitmap colorBitmap = new Bitmap(..);
WriteableBitmap colorWB = new WriteableBitmap(..);

//choose appropriate bytes as per your pixel format, I'll cheat here an just pick 4
int bytesPerPixel = 4;

//rectangles will be used to identify what bits change
Rectangle colorBitmapRectangle = new Rectangle(0, 0, colorBitmap.Width, colorBitmap.Height);
Int32Rect colorBitmapInt32Rect = new Int32Rect(0, 0, colorWB.PixelWidth, colorWB.PixelHeight);

Conversion Code

private void ConvertBitmapToWritableBitmap()
{
    BitmapData data = colorBitmap.LockBits(colorBitmapRectangle, ImageLockMode.WriteOnly, colorBitmap.PixelFormat);

    colorWB.WritePixels(colorBitmapInt32Rect, data.Scan0, data.Width * data.Height * bytesPerPixel, data.Stride);

    colorBitmap.UnlockBits(data); 
}

Implementation Example

//do stuff to your bitmap
ConvertBitmapToWritableBitmap();
Image.Source = colorWB;

The result is a steady 10-13% CPU, 70-150MB RAM, and the garbage collector only ran twice in a 6 minute run.

Quillan answered 31/12, 2015 at 2:56 Comment(1)
No, sorry, I'm unable to repro your error. Based on your error tho, I think your trying to access the Bitmap directly. See, what's going on is that your copying the bitmap from the Kinect stream and and writing it to your own WritableBitmap all in the conversion code. Try double checking the sequence of locking and unlocking, that your moving between Bitmap -> BitmapData -> WritableBitmap, and that the rectangle is the proper size including the z-axis = bytesPerPixel. Good luckQuillan
D
0

This is a great(!!) post, although with all the comments and suggestions, it took me an hour to figure out the pieces. So here is a call to get the BitMapSource with SafeHandles, and then an example usage of it to create a .PNG image file. At the very bottom are the 'usings' and some of the references. Of course, none of the credit is mine, I am just the scribe.

private static BitmapSource CopyScreen()
{
    var left = Screen.AllScreens.Min(screen => screen.Bounds.X);
    var top = Screen.AllScreens.Min(screen => screen.Bounds.Y);
    var right = Screen.AllScreens.Max(screen => screen.Bounds.X + screen.Bounds.Width);
    var bottom = Screen.AllScreens.Max(screen => screen.Bounds.Y + screen.Bounds.Height);
    var width = right - left;
    var height = bottom - top;

    using (var screenBmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
    {
        BitmapSource bms = null;

        using (var bmpGraphics = Graphics.FromImage(screenBmp))
        {
            IntPtr hBitmap = new IntPtr();
            var handleBitmap = new SafeHBitmapHandle(hBitmap, true);

            try
            {
                bmpGraphics.CopyFromScreen(left, top, 0, 0, new System.Drawing.Size(width, height));

                hBitmap = screenBmp.GetHbitmap();

                using (handleBitmap)
                {
                    bms = Imaging.CreateBitmapSourceFromHBitmap(
                        hBitmap,
                        IntPtr.Zero,
                        Int32Rect.Empty,
                        BitmapSizeOptions.FromEmptyOptions());

                } // using

                return bms;
            }
            catch (Exception ex)
            {
                throw new ApplicationException($"Cannot CopyFromScreen. Err={ex}");
            }

        } // using bmpGraphics
    }   // using screen bitmap
} // method CopyScreen

Here is the usage, and also the "Safe Handle" class:

private void buttonTestScreenCapture_Click(object sender, EventArgs e)
{
    try
    {
        BitmapSource bms = CopyScreen();
        BitmapFrame bmf = BitmapFrame.Create(bms);

        PngBitmapEncoder encoder = new PngBitmapEncoder();
        encoder.Frames.Add(bmf);

        string filepath = @"e:\(test)\test.png";
        using (Stream stm = File.Create(filepath))
        {
            encoder.Save(stm);
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Err={ex}");
    }
}

public class SafeHBitmapHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    [System.Runtime.InteropServices.DllImport("gdi32.dll")]
    public static extern int DeleteObject(IntPtr hObject);

    [SecurityCritical]
    public SafeHBitmapHandle(IntPtr preexistingHandle, bool ownsHandle)
        : base(ownsHandle)
    {
        SetHandle(preexistingHandle);
    }

    protected override bool ReleaseHandle()
    {
        return DeleteObject(handle) > 0;
    }
}

And finally a look at my 'usings':

using System;
using System.Linq;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Media.Imaging;
using System.Windows.Interop;
using System.Windows;
using System.IO;
using Microsoft.Win32.SafeHandles;
using System.Security;

The DLLs referenced included: * PresentationCore * System.Core * System.Deployment * System.Drawing * WindowsBase

Drowse answered 19/1, 2018 at 13:37 Comment(1)
Your code for calculating screen bounds (Screen.AllScreens.Min/Max) can be replaced by SystemParameters.VirtualScreenLeft/Top/Width/Height. This eliminates the need of referencing System.Windows.Forms, which isn't part of WPF, and makes the code faster especially when you have multiple screens.Tropic
V
0

In my case it did not work directly with this method. I had to add a clean garbage collector in addition

    using (PaintMap p = new PaintMap())
    {
        System.Drawing.Image i = p.AddLineToMap("1");
        System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(i, 8419, 5953);
        IntPtr hBitmap = bmp.GetHbitmap();

        var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
        Image2.ImageSource = bitmapSource;

        DeleteObject(hBitmap);

        System.GC.Collect();
    }
Vannoy answered 30/4, 2020 at 20:30 Comment(0)
T
-1

I have a solution for someone who want to load image from memory or other class

 public static InteropBitmap Bitmap2BitmapImage(Bitmap bitmap)
        {
            try
            {
                var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(bitmap.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());

                return (InteropBitmap)source;

            }
            catch (Exception e)
            {
                MessageBox.Show("Convertion exception: " + e.Message + "\n" +e.StackTrace);
                return null;
            }
        }

And then I use it the set the source of an image

CurrentImage.Source = ImageConverter.Bitmap2BitmapImage(cam.Bitmap);

Image is the following definition

<Image x:Name="CurrentImage" Margin="5" StretchDirection="Both"
                Width="{Binding Width}"
                Height="{Binding Height}">
                </Image>
Tipstaff answered 13/11, 2018 at 15:38 Comment(1)
The code is essentially the same as that in the question. It doesn't solve the memory leak problem at all.Tropic

© 2022 - 2024 — McMap. All rights reserved.