Large, 1 bit-per-pixel bitmap causes OutOfMemoryException

北慕城南 提交于 2019-12-01 16:48:53

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;
}
Aybe

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!

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!