【问题标题】:Large, 1 bit-per-pixel bitmap causes OutOfMemoryException大的,每像素 1 位的位图会导致 OutOfMemoryException
【发布时间】:2014-12-05 16:19:01
【问题描述】:

我想做的是从磁盘加载图像并从中创建一个BitmapSource
图像为 14043 像素 x 9933 像素,为黑白 (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 可用)

启动应用后,发现此方法的内存使用峰值非常高:(初始峰值后可用 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:

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

【问题讨论】:

  • 你在计算什么? (公式)
  • 如果你的图片是 32bits ARGB,会有 4 字节/像素,图片会消耗 14043*9933*4 = 532Mo。
  • 得到它,这是一个黑白图像,所以 8 像素/字节。实际上它导致 16.7 MB。
  • @SebastianL 14043px x 9933px 是 139489119 像素(或位)是 17436139 字节是 17027 kbytes 是 16.7 mbytes
  • 请记住,OutOfMemory 异常与可用 RAM 无关。它取决于虚拟内存(每个 32 位进程限制为 2 或 4 GB)。您可以通过 Process Explorer 或 Process Hacker 等第三方应用查看进程的虚拟内存量

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


【解决方案1】:

编辑:我的结论是这是一种 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,因为它依赖于 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)每个像素使用一位。
  • 谢谢Aybe!它真的很好用!我想没有办法绕过存储在磁盘上的 png 文件,对吧? :( 因为我正在加载一个 tif 并且 pngcs 库会在将其显示为位图之前将其保存为 png。我需要访问像素数据的原因是因为我必须在显示之前对图像进行操作。你不需要必须解决这个问题,因为你几乎解决了这个问题。无论如何我都会接受答案:D
  • 很高兴看到它有帮助!快速搜索了以下 TIFF 库:bitmiracle.com/libtiff(虽然还没有测试过)
【解决方案2】:

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

【讨论】:

  • (哎呀,打算在你之前发布,但你已经更快了)你的代码问题:该进程使用了​​近 580Mb 的内存:D
  • 另外,虽然您对WriteableBitmap 的想法确实是正确的,但您可以看到它总是会推动进程吃掉与第一种情况相同的内存;只需通过该行并查看任务管理器。
  • @Aybe 只是冻结位图。现在我的测试应用程序消耗不到 30 MB。
  • +1 很好的答案,我应该更多地考虑冻结对象...问题是文档中确实忽略了“冻结”方面,因此我们倾向于忽略它,而它确实非常强大。
  • +1 因为它解决了问题。问题是我使用缩放和平移功能再次导致内存异常。但初始 RAM 消耗确实是 30 MB!如果有人想继续解决这个问题:我从这里使用 ZoomBox:wpftoolkit.codeplex.com
猜你喜欢
  • 2013-06-14
  • 1970-01-01
  • 1970-01-01
  • 2012-11-03
  • 1970-01-01
  • 2012-03-11
  • 1970-01-01
  • 1970-01-01
  • 2018-04-19
相关资源
最近更新 更多