如何在不占用大量内存的情况下显示图片

13

我正在处理一个银光项目,用户可以创建自己的拼贴画。

问题

当使用BitmapImage类加载大量图像时,Silverlight会占用巨大且不合理的内存。150张图片,每张最多只占用4.5MB的空间,却占用了约1.6GB的内存--最终导致内存异常。

由于用户选择他们自己的照片,所以我是通过流来加载它们的。

我正在寻找

一种类、方法或某个过程来消除大量的RAM被耗尽。速度是一个问题,所以我不想在不同的图像格式之间进行转换或其他任何操作。快速的调整大小的解决方案可能有效。

我尝试使用WriteableBitmap将图像渲染到其中,但我发现这种方法迫使我重新发明轮子,当涉及到拖放和其他我希望用户能够对图像执行的操作时。


所以你想要一种高效的解决方案,但你又不想使用那些提高效率的方法,比如在较低级别处理图像?为什么呢?我不熟悉Silverlight,希望你能找到解决方案,但有时候你确实需要动起手来去做一些工作。 - Ed S.
问题仅在于内存效率。我不需要快速渲染或图像数据本身的快速修改——我只需要使用不超过实际JPEG数据的内存来表示它,并在稍后在PDF中组合它。我使用的PDF框架要求我投入JPEG数据流,但我没有找到一个好的方法将BitmapImage转换为JPEG流。 - Michael Sondergaard
1
你能否包含一些代码来说明你的程序如何工作,以及你的图像通常有多大?你一次显示多少张图片? - Mikael Svenson
6个回答

6
我建议的方法是在加载下一个流之前,将每个流加载并调整大小为缩略图(例如,640x480),然后让用户使用较小的图像。一旦准备好生成PDF,按顺序重新加载原始流中的JPEG,每次加载下一个位图之前将前一个位图处理掉。

这将把你的4.5兆字节图像转换为约0.9兆字节的图像。通过使用16位色,您可以将其降至0.6兆字节。现在,您的150张图片大约需要60兆字节。虽然仍然很多,但更加可行。只要您在最终渲染时使用原始图像,就不会有问题。 - Aric TenEyck
我已经尝试过这样做,它实际上会拖慢处理能力。我的想法是,当silverlight渲染图像时,它以某种方式已经在这样做。是否可以强制位图图像放弃其资源,同时将最终渲染的图像保留在内存中? - Michael Sondergaard
WPF 至少在合成过程中完成此操作。因此,缩放是由图形硬件实时完成的。我怀疑 Silverlight 做了类似的事情。如果是这样,那么答案是否定的,至少对于你来说不是一种可访问的方式。 - Russell Mull

4
我猜你可能正在做类似这样的事情:

我猜测您正在做以下工作:

Bitmap bitmap = new Bitmap (filename of jpeg);

然后执行:

OnPaint (...)
{
   Graphics g = ....;
   g.DrawImage (bitmap, ...);
}

每次绘制时,这将调整巨大的JPEG图像大小以适应屏幕显示。我猜测您的JPEG大小约为2500x2000像素。将JPEG加载到位图中时,位图加载代码会解压缩数据并将其存储为RGB数据格式,以便易于呈现(即与显示器相同的像素格式)或作为称为设备无关位图(也称为DIBitmap)的东西。这些位图需要比压缩的JPEG更多的RAM来存储。
您当前的实现已经在进行格式转换和调整大小,但是以一种低效的方式进行,即每次呈现时将巨大的图像缩小到屏幕大小。
理想情况下,您希望加载图像并将其缩小。 .Net有一个系统可以做到这一点:
Bitmap bitmap = new Bitmap (filename of JPEG);
Bitmap thumbnail = bitmap.GetThumbnailImage (width, height, ....);
bitmap.Dispose (); // this releases all the unmanged resources and makes the bitmap unusable - you may have been missing this step
bitmap = null; // let the GC know the object is no longer needed

在这里,width和height是所需缩略图的尺寸。但是,这可能会产生不太理想的图像(但如果存在任何嵌入式缩略图数据,它将使用它们以加快速度),如果这种情况发生,请执行位图->位图调整大小。

创建PDF文件时,您需要重新加载JPEG数据,但从用户的角度来看,这是可以接受的。我相信只要您有一些反馈告诉用户正在处理数据,用户就不会介意等待短暂的时间将其导出为PDF。您还可以在后台线程中执行此操作,让用户在另一个拼贴上工作。


1
你说得没错,但在Silverlight中,我使用从这些类派生的类。我必须使用BitmapImage,因为没有其他等效的类。这意味着我无法直接控制格式转换,并且我没有任何方法从JPEG中获取缩略图。因此,我的唯一选择是从原始图像创建一个BitmapImage,并使用它来创建缩略图 - 然而,这时我遇到了Alex上面指出的问题。谢谢您的回复。 - Michael Sondergaard
@Sheeo:我认为Image.Dispose方法可以解决Alex提出的问题,因为你告诉系统你不再需要这些资源了——文档中指出Dispose方法会释放所有未托管的资源(而大部分都是未托管的——DIBitmap)。 - Skizz

1
你可能正在经历的是垃圾回收的一个鲜为人知的事实,这也曾经困扰过我。如果一个对象足够大(我不记得具体的界限在哪里),垃圾回收会决定即使当前范围内没有任何东西与该对象相关联(在你和我的情况下,对象是图像),它仍然将图像保留在内存中,因为它已经决定,以防你再次需要该图像,保留它比删除它并稍后重新加载它更便宜。

是的,我在寻找答案时发现了这个。目前还没有解决方法,对吗? - Michael Sondergaard
System.GC.Collect()和System.GC.WaitForPendingFinalizers()似乎是我能想到的帮助它的方法,但可能还有更好的方式。 - Alex

1

这不是一个完整的解决方案,但如果你要在位图和JPEG之间进行转换(反之亦然),你需要了解FJCore图像库。它很简单易用,并可以让你做一些事情,例如调整JPEG图像的大小或将其移动到不同的质量。如果你正在使用Silverlight进行客户端图像处理,这个库可能不够,但肯定是必要的。

你还应该考虑如何向用户呈现图片。如果你正在使用Silverlight制作拼贴画,那么用户会同时操作所有150张图片,所以可能无法使用虚拟化控件。但正如其他人所说,你还应该确保不要基于全尺寸的JPEG文件来呈现位图。一个1MB压缩的JPEG文件可能会扩展到一个10MB的位图,这很可能是你的问题所在。确保你基于更小的(低质量和调整大小后的)JPEG文件呈现给用户的图像。


谢谢你的回答,我也遇到了FJCore的问题--例如,在解码时与通过BitmapImage加载它们相比,JPEG实际上变得更大了。你回答的底部部分已经在上面讨论过了。 - Michael Sondergaard

0
最终对我有用的解决方案是使用WriteableBitmapEX来执行以下操作:
当然,如果图像已经足够小而可以存储在内存中,我只使用缩略图。
我遇到的问题是WriteableBitmap没有无参构造函数,但使用0,0作为大小进行初始化,然后加载源会自动设置这些。这对我来说不是很自然。
感谢大家的帮助!
private WriteableBitmap getThumbnailFromBitmapStream(Stream bitmapStream, PhotoFrame photoFrame)
    {
        WriteableBitmap inputBitmap = new WriteableBitmap(0,0);
        inputBitmap.SetSource(bitmapStream);

        Size thumbnailSize = getThumbnailSizeFromWriteableBitmap(inputBitmap, photoFrame.size);

        WriteableBitmap thumbnail = new WriteableBitmap(0,0);
        thumbnail = inputBitmap.Resize((int)thumbnailSize.Width, (int)thumbnailSize.Height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);

        return thumbnail;
    }

0

减少内存使用的另一种方法是: 不要加载此时不可见的图片,而是在用户滚动页面时再加载它们。这是网页开发人员用来提高页面加载速度的方法。对你来说,这是不将所有图片存储在内存中的方法。

我认为更好的方法是不要在运行时创建缩略图,而是将它们存储在完整大小的图片附近,并仅获取链接。需要时,您随时可以获取到完整大小的图片链接并加载它。


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