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

我想做的是从磁盘加载图像并从中创建一个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 colors = new List(); 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免费) 在此处输入图像描述

在启动应用程序后,它发现这种方法具有非常高的内存使用峰值:(初始峰值后可免费使用497 MB) 克莱门斯解决方案

我尝试了几件事,发现@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免费)(正如你所看到的,蓝线在右侧增加了一点点) 克莱门斯溶液改性

有了这些知识,我相信我的图像控制也有问题。 将writeableBitmap分配给图像控件时,峰值开始。 这就是我所有的xaml:

      

冻结位图很重要。 此外,由于您使用的是每像素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; } 

或者,您可以在没有BitmapPalette的情况下使用BlackWhite格式:

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

编辑:我得出的结论是,这是一种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!

在此处输入图像描述

在此处输入图像描述

你需要什么(.NET 4.5)

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

为什么我使用PNGCS?

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

码:

 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图像。