Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Large, 1 bit-per-pixel bitmap causes OutOfMemoryException

What I'd like to do is to load an image from disk and create a BitmapSource from it.
The image is 14043px x 9933px and is b/w (1bpp). But I run into a OutOfMemoryException because the following code consumes about 800 MB RAM.

The following code generates an ImageSource in the dimensions of my specific file.
I did this to see if I could make it work without using the actual file on my disk.

public System.Windows.Media.ImageSource getImageSource(){

    int width = 14043;
    int height = 9933;

    List<System.Windows.Media.Color> colors = new List<System.Windows.Media.Color>();
    colors.Add(System.Windows.Media.Colors.Black);
    colors.Add(System.Windows.Media.Colors.White);

    BitmapPalette palette = new BitmapPalette(colors);
    System.Windows.Media.PixelFormat pf = System.Windows.Media.PixelFormats.Indexed1;

    int stride = width / pf.BitsPerPixel;

    byte[] pixels = new byte[height * stride];

    for (int i = 0; i < height * stride; ++i)
    {
         if (i < height * stride / 2)
         {
               pixels[i] = 0x00;
         }
         else
         {
               pixels[i] = 0xff;
         }
    }

    BitmapSource image = BitmapSource.Create(
      width,
      height,
      96,
      96,
      pf,
      palette,
      pixels,
      stride);



    return image;
}

In my calculation the image should consume about 16.7 MB.
Also, I cannot specify a cache option when using BitmapSource.create. But the image must be cached on load.

The return value of this method is set as the source of an image control.


ISSUE REOPENED

After @Clemens posted the answer, wich worked very well in the first place. I noticed a very bad behaviour while inspecting my TaskManager. This is the code I am using, it is excatly the same as @Clemens answer.

public ImageSource getImageSource(){
   var width = 14043;
   var height = 9933;

   var stride = (width + 7) / 8;
   var pixels = new byte[height * stride];

   for (int i = 0; i < height * stride; i++){
      pixels[i] = 0xAA;
   }

   WriteableBitmap bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.BlackWhite, null);
   bitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0);
   bitmap.Freeze();
   return bitmap;
}

Before running any code, my taskmanager shows the following: (1057 MB free) enter image description here

After starting the app it turnes out that this method has a very high peak of memory usage: (497 MB free after initial peak) Clemens Solution

I tried a couple things and found out that @Clemens routine probably isn't the problem. I changed the code to this:

private WriteableBitmap _writeableBitmap; //Added for storing the bitmap (keep it in scope)

public ImageSource getImageSource(){
   var width = 14043;
   var height = 9933;

   var stride = (width + 7) / 8;
   var pixels = new byte[height * stride];

   for (int i = 0; i < height * stride; i++){
      pixels[i] = 0xAA;
   }

   _writeableBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.BlackWhite, null);
   _writeableBitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0);
   _writeableBitmap.Freeze();
   return null; //Return null (Image control source will be set to null now but bitmap still stored in private field)
}

I wanted to keep the bitmap in memory but not affect the image control, this is the result: (997 MB free) (As you can see, the blue line increases just a tiny bit at the right side) Clemens Solution Modified

With this knowledge I believe there is something wrong with my image control as well. The peak starts when the writeableBitmap is assigned to the image control. This is all my xaml:

<Window x:Class="TifFileViewer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TifFileViewer"
        xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
        Title="MainWindow" Height="563" Width="1046">
    <Grid Margin="10">
        <Image x:Name="imageControl"/>
    </Grid>
</Window>
like image 709
Noel Widmer Avatar asked Oct 10 '14 13:10

Noel Widmer


2 Answers

EDIT: I'd conclude that this is a DIY approach since as @Clemens cleverly pointed out in his answer, freezing the bitmap does the same but with a one-liner.

You really need to get your hands dirty to achieve what you're looking for ;)

Explanations

(with correction from @Clemens)

The .NET framework doesn't treat less than 8 bit-per-pixel images very well. It systematically converts them to 32BPP which in my case brought my process to nearly 200Mb. Read here it's a bug or it's by design.

Whether using WriteableBitmap (with/without pointers) or BitmapSource.Create it will consume that much memory, BUT; there's only one place (BitmapImage) where it's behaving appropriately and fortunately it's a critical place for achieving what you're looking for!

Note: the framework will accept less than or equal to 8 bits per pixel images only if 1 byte equals 1 pixel. As you and I are seeing this, a 1 bit per pixel image means 1 byte = 8 pixels; I've followed this norm. While some could say this as a bug, it's probably a convenience for the dev for not dealing with bits directly.

Solution

(specifically for a 1BPP image)

As I said, you will have to get your hands dirty but I'll explain everything so you should get up and running pretty quickly ;)

What I did:

  • Generated the image manually at 1BPP (which is effectively 17Mb)
  • wrote that result to a .PNG file
  • created a BitmapImage out of that PNG file

The application memory usage does not go up, actually it gets to 60Mb but goes down to 35Mb shortly later probably because the garbage collector collects the byte[] used initially. Anyway it never reaches 200 nor 800 Mb as you've experienced!

enter image description here

enter image description here

What you need (.NET 4.5)

  • download the PNGCS library from https://code.google.com/p/pngcs/
  • rename Pngcs45.dll to Pngcs.dll otherwise a FileNotFoundException will occur
  • add a reference to that DLL
  • use the code below

Why did I use PNGCS?

Because the same issues as laid out above apply to PngBitmapEncoder from WPF since it relies on BitmapFrame for adding content to it.

Code:

using System;
using System.Windows;
using System.Windows.Media.Imaging;
using Hjg.Pngcs;

namespace WpfApplication3
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            int width = 14043;
            int height = 9933;
            int stride;
            byte[] bytes = GetBitmap(width, height, out stride);
            var imageInfo = new ImageInfo(width, height, 1, false, true, false);

            PngWriter pngWriter = FileHelper.CreatePngWriter("test.png", imageInfo, true);
            var row = new byte[stride];
            for (int y = 0; y < height; y++)
            {
                int offset = y*stride;
                int count = stride;
                Array.Copy(bytes, offset, row, 0, count);
                pngWriter.WriteRowByte(row, y);
            }
            pngWriter.End();

            var bitmapImage = new BitmapImage();
            bitmapImage.BeginInit();
            bitmapImage.UriSource = new Uri("test.png", UriKind.Relative);
            bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
            bitmapImage.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
            bitmapImage.EndInit();
            Image1.Source = bitmapImage;
        }

        private byte[] GetBitmap(int width, int height, out int stride)
        {
            stride = (int) Math.Ceiling((double) width/8);
            var pixels = new byte[stride*height];
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    var color = (byte) (y < height/2 ? 0 : 1);
                    int byteOffset = y*stride + x/8;
                    int bitOffset = x%8;
                    byte b = pixels[byteOffset];
                    b |= (byte) (color << (7 - bitOffset));
                    pixels[byteOffset] = b;
                }
            }

            return pixels;
        }
    }
}

Now you can enjoy your 1BPP image.

like image 156
aybe Avatar answered Nov 06 '22 01:11

aybe


It is important that you freeze the bitmap. Also, since you are using a 1 bit per pixel format, you should calculate the pixel buffer's stride as width / 8.

The following method creates a bitmap with pixels set to alternating black and white.

public ImageSource CreateBitmap()
{
    var width = 14043;
    var height = 9933;

    var stride = (width + 7) / 8;
    var pixels = new byte[height * stride];

    for (int i = 0; i < height * stride; i++)
    {
        pixels[i] = 0xAA;
    }

    var format = PixelFormats.Indexed1;
    var colors = new Color[] { Colors.Black, Colors.White };
    var palette = new BitmapPalette(colors);

    var bitmap = BitmapSource.Create(
        width, height, 96, 96, format, palette, pixels, stride);

    bitmap.Freeze(); // reduce memory consumption
    return bitmap;
}

Alternatively you could use the BlackWhite format without a BitmapPalette:

    var format = PixelFormats.BlackWhite;

    var bitmap = BitmapSource.Create(
        width, height, 96, 96, format, null, pixels, stride);

EDIT: If you create a WriteableBitmap instead of using BitmapSource.Create the large bitmap also works with an Image control in a Zoombox:

public ImageSource CreateBitmap()
{
    ...
    var bitmap = new WriteableBitmap(width, height, 96, 96, format, palette);
    bitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0);
    bitmap.Freeze();
    return bitmap;
}
like image 37
Clemens Avatar answered Nov 05 '22 23:11

Clemens