大型,每像素1比特的位图导致OutOfMemoryException异常

7
我想做的是从磁盘加载图像并创建一个BitmapSource
该图像的大小为14043像素x9933像素,是黑白(1bpp)图像。但由于以下代码约占用800 MB RAM,因此我遇到了OutOfMemoryException错误。
以下代码生成我的特定文件尺寸的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发布答案后,一开始它确实有效。但是当我检查我的任务管理器时,发现了非常糟糕的行为。这是我正在使用的代码,它与@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 基于此,我认为我的图像控件存在问题。峰值开始于将可写位图分配给图像控件时。以下是我的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>

如果您的图像是32位ARGB格式,每个像素将占用4个字节,该图像将消耗1404399334 = 532Mo的内存空间。 - Orace
顺便提一下,还有一个PixelFormats.BlackWhite :o) - Orace
@Orace 你是对的。我也尝试过这个方法。但是它导致了相同的问题,所以我又回到了上面的代码,呵呵。 - Noel Widmer
@Orace 不是32,我的应用程序没有调用这个方法时只消耗不到20MB,所以800MB必须来自上面的代码。奇怪的是它在你的系统上表现得稍微好一些。 - Noel Widmer
1
请记住,OutOfMemory异常与可用RAM无关。它取决于虚拟内存(每个32位进程限制为2或4 GB)。您可以通过第三方应用程序(如Process Explorer或Process Hacker)查看进程的虚拟内存量。 - DarkWanderer
显示剩余12条评论
2个回答

6

编辑: 我认为这是一种DIY方法,因为正如@Clemens在他的答案中聪明地指出的那样,冻结位图可以用一行代码实现相同的效果。

你需要动手才能实现你想要的东西 ;)

解释

(经过@Clemens的更正)

.NET框架不能很好地处理少于8个位每像素的图像。它会将它们系统地转换为32BPP,这在我的情况下使我的进程几乎达到了200Mb。在这里阅读它是一个错误还是一个设计问题。

无论是使用WriteableBitmap(有/没有指针)还是BitmapSource.Create,它都会消耗那么多内存,但是;只有一个地方(BitmapImage)它的行为是适当的,而且幸运的是,这是实现你所要寻找的关键之一!

注意:框架只接受每像素小于或等于8位的图像,如果1字节等于1像素。正如你和我看到的,每像素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图像。

PixelFormats.Indexed1PixelFormats.BlackWhite(不是BlackAndWhite)都使用每像素一位的位图格式。 - Clemens
谢谢 Aybe!它真的很好用!我猜没有绕过存储在磁盘上的 png 文件的方法,对吧? :( 因为我正在加载一个 tif 文件,而 pngcs 库会将其保存为 png 格式,然后再将其显示为位图。我需要访问像素数据的原因是因为我必须在显示之前操作图像。你不必解决这个问题,因为你已经基本解决了这个问题。无论如何,我会接受这个答案 :D - Noel Widmer
很高兴看到它有所帮助!快速搜索找到了以下用于TIFF的库:http://bitmiracle.com/libtiff/(尚未测试) - aybe

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格式:

    var format = PixelFormats.BlackWhite;

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

编辑: 如果您创建一个WriteableBitmap而不是使用BitmapSource.Create,则大位图也可以在Zoombox中的Image控件中使用:

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

此外,虽然您关于 WriteableBitmap 的想法确实是正确的,但您会发现它不可避免地会推动进程消耗与第一种情况相同的内存;只需通过该行并查看任务管理器即可。 - aybe
3
只需冻结位图即可。现在我的测试应用程序消耗少于30 MB 的内存。 - Clemens
+1 很棒的答案,我应该更多地考虑冻结对象...问题在于“冻结”方面在文档中真的被忽视了,所以我们倾向于忽略它,而它确实非常强大。 - aybe
+1 因为它解决了问题。问题是我使用了缩放和平移功能,这又导致了内存异常。但初始 RAM 消耗确实只有 30 MB!如果有人想继续解决这个问题:我正在使用来自此处的 ZoomBox:https://wpftoolkit.codeplex.com/ - Noel Widmer
冻结位图为什么很重要?冻结有什么影响?已经确定它可以解决问题,但是为什么? - cbr
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接