大的,每像素1位的位图导致OutOfMemoryException

时间:2014-10-10 13:16:53

标签: c# wpf bitmap out-of-memory

我要做的是从磁盘加载图片并从中创建BitmapSource
图像为14043px x 9933px,为b / w(1bpp)。但我遇到OutOfMemoryException,因为以下代码消耗大约800 MB RAM。

以下代码在我的特定文件的维度中生成ImageSource
我这样做是为了看看我是否可以在不使用磁盘上的实际文件的情况下使其工作。

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

在我的计算中,图像应该消耗大约16.7 MB 此外,使用BitmapSource.create时,我无法指定缓存选项。但是图像必须在加载时缓存
此方法的返回值被设置为图像控件的源。


问题已重新审核

在@Clemens发布答案之后,首先做得非常好。检查我的TaskManager时,我注意到了一个非常糟糕的行为。这是我正在使用的代码,它与@Clemens的答案非常相似。

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

在运行任何代码之前,我的任务管理器显示以下内容:(1057 MB免费) enter image description here

启动应用程序后,它发现此方法具有非常高的内存使用峰值:(初始峰值后可用497 MB) Clemens Solution

我尝试了几件事,发现@Clemens例程可能不是问题所在。我将代码更改为:

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)
}

我想将位图保留在内存中但不影响图像控制,结果是:(997 MB免费) (如您所见,蓝线在右侧略微增加) Clemens Solution Modified

凭借这些知识,我相信我的图像控制也有问题。将writeableBitmap分配给图像控件时,峰值开始。这就是我所有的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>

2 个答案:

答案 0 :(得分:6)

编辑:我得出结论,这是一种DIY方法,因为@Clemens在他的回答中巧妙地指出,冻结位图的方法是相同的但只有一行。

你真的需要亲自动手去实现你想要的东西;)

说明

(来自@Clemens的修正)

.NET框架不能很好地处理少于8位的每像素图像。它系统地将它们转换为32BPP,在我的情况下,我的过程将近200Mb。在这里阅读它是一个错误或它是设计的。

无论是使用WriteableBitmap(有/无指针)还是BitmapSource.Create,都会消耗那么多内存,但是;只有一个地方(BitmapImage)表现得恰到好处,幸运的是,它是实现您所寻找目标的重要场所!

注意:只有当1个字节等于1个像素时,框架才会接受小于或等于8位/像素的图像。正如您和我所看到的那样,每像素1位图像意味着1个字节= 8个像素;我遵循了这个规范。虽然有些人可能会将此视为一个错误,但对于不直接处理位的开发者而言,这可能是一种方便。

解决方案

(特别是1BPP图像)

正如我所说,你必须弄脏你的手,但我会解释一切,所以你应该快速起床跑步;)

我做了什么:

  • 在1BPP(实际上是17Mb)手动生成图像
  • 将结果写入.PNG文件
  • 从该PNG文件中创建了BitmapImage

应用程序内存使用量没有增加,实际上它达到了60Mb但很快就降到了35Mb,可能是因为垃圾收集器收集了最初使用的byte[]。无论如何,它你从未经历过200或800 Mb!

enter image description here

enter image description here

您需要什么(.NET 4.5)

  • https://code.google.com/p/pngcs/
  • 下载PNGCS库
  • Pngcs45.dll重命名为Pngcs.dll,否则将发生FileNotFoundException
  • 添加对该DLL的引用
  • 使用以下代码

为什么我使用PNGCS?

因为上面列出的相同问题适用于WPF中的PngBitmapEncoder,因为它依赖于BitmapFrame向其添加内容。

<强>代码:

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

现在您可以享受您的1BPP图像了。

答案 1 :(得分:4)

冻结位图非常重要。此外,由于您使用的是每像素1位格式,因此您应将像素缓冲区的步幅计算为width / 8

以下方法创建一个位图,其像素设置为交替黑白。

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

或者,您可以使用BlackWhite格式而不使用BitmapPalette:

    var format = PixelFormats.BlackWhite;

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

修改 如果您创建WriteableBitmap而不是使用BitmapSource.Create,则大位图也适用于缩放框中的图像控件:

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