将 Tiff 的帧组合成单个 png 会产生内存不足异常 C#

Combining Frames of a Tiff into a single png gives Out of Memory Exception C#

首先,我将 tiff 的帧保存到位图列表中:

public Bitmap SaveTiffAsTallPng(string filePathAndName)
{
    byte[] tiffFile = System.IO.File.ReadAllBytes(filePathAndName);
    string fileNameOnly = Path.GetFileNameWithoutExtension(filePathAndName);
    string filePathWithoutFile = Path.GetDirectoryName(filePathAndName);
    string filename = filePathWithoutFile + "\" + fileNameOnly + ".png";

    if (System.IO.File.Exists(filename))
    {
        return new Bitmap(filename);
    }
    else
    {
        List<Bitmap> bitmaps = new List<Bitmap>();
        int pageCount;

        using (Stream msTemp = new MemoryStream(tiffFile))
        {
            TiffBitmapDecoder decoder = new TiffBitmapDecoder(msTemp, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
            pageCount = decoder.Frames.Count;

            for (int i = 0; i < pageCount; i++)
            {
                System.Drawing.Bitmap bmpSingleFrame = Worker.BitmapFromSource(decoder.Frames[i]);
                bitmaps.Add(bmpSingleFrame);
            }

            Bitmap bmp = ImgHelper.MergeImagesTopToBottom(bitmaps);

            EncoderParameters eps = new EncoderParameters(1);
            eps.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 16L);
            ImageCodecInfo ici = Worker.GetEncoderInfo("image/png");

            bmp.Save(filename, ici, eps);

            return bmp;
        }
    }
}

然后我将该位图列表传递给一个单独的函数来进行实际的合并:

public static Bitmap MergeImagesTopToBottom(IEnumerable<Bitmap> images)
{
    var enumerable = images as IList<Bitmap> ?? images.ToList();
    var width = 0;
    var height = 0;

    foreach (var image in enumerable)
    {
        width = image.Width > width
            ? image.Width
            : width;
        height += image.Height;
    }

    Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format16bppGrayScale);                                      

    Graphics g = Graphics.FromImage(bitmap);//gives Out of Memory Exception

    var localHeight = 0;
    foreach (var image in enumerable)
    {
        g.DrawImage(image, 0, localHeight);
        localHeight += image.Height;
    }

    return bitmap;                     
}

但我通常会遇到内存不足异常,具体取决于 tiff 中的帧数。即使只有 5 张大约 2550 像素 x 3300 像素的图像也足以导致错误。那只有大约 42MB,最终保存为 png,总大小为 2,550px x 16,500px,磁盘上只有 1.5MB。我什至在 web.config 中使用此设置: <gcAllowVeryLargeObjects enabled=" true" /> 其他详细信息:我正在使用 64 位 Windows 7 和 16GB RAM(我通常 运行 在大约 65% 的 ram 使用),我的代码是 运行ning in Visual Studio 2013 在 asp.net MVC 项目中。我运行将项目设置为 32 位,因为我使用的是仅在 32 位中可用的 Tessnet2。不过,我认为我应该有足够的内存来一次处理超过 5 张图像。有没有更好的方法来解决这个问题?如果可以的话,我宁愿不必求助于付费框架。我觉得这是我应该能够使用开箱即用的 .net 代码免费完成的事情。谢谢!

我没有可用于测试的数据,所以无法测试您的代码。然而,很明显至少您可以在这里以不止一种明显的方式节省内存。

进行这些更改:

首先,不要读取所有字节并使用 MemoryStream:您正在将整个文件复制到内存,然后使用 BitmapCacheOption.Default 创建 Decoder,这应该再次将整个内存流加载到解码器中...

消除tiffFile数组。在using语句中,在文件上打开一个FileStream;并使用 BitmapCacheOption.None 创建解码器 --- 解码器没有内存存储。

然后你为每一帧创建一个 BitMaps 的完整列表!相反,只需在解码器上迭代帧即可获得目标大小(BitmapFramePixelWidth)。用它来创建你的目标 BitMap;然后迭代帧并在那里绘制每个帧:将每个帧的 BitMap 放在一个 using 块中,并在将其绘制到目标后立即处理每个帧。

将您的本地位图 bmp 变量移到 using 块之外,以便在您创建该位图后所有先前的内容都可以释放。然后在 using 块之外将其写入您的文件。

很明显,您正在制作整个图像的两个完整副本,加上为每个帧制作每个 BitMap,再加上制作最终的 BitMap ...这是很多暴力复制。它都在 using 块中,因此在您完成新文件的写入之前,不会留下任何内存。

可能还有更好的方法将 Streams 传递给解码器并使用 Streams 制作位图,这不需要将所有字节转储到内存中。

public Bitmap SaveTiffAsTallPng(string filePathAndName) {
    string pngFilename = Path.ChangeExtension(filePathAndName), "png");
    if (System.IO.File.Exists(pngFilename))
        return new Bitmap(pngFilename);
    else {
        Bitmap pngBitmap;
        using (FileStream tiffFileStream = File.OpenRead(filePathAndName)) {
            TiffBitmapDecoder decoder
                    = new TiffBitmapDecoder(
                            tiffFileStream,
                            BitmapCreateOptions.PreservePixelFormat,
                            BitmapCacheOption.None);
            int pngWidth = 0;
            int pngHeight = 0;
            for (int i = 0; i < decoder.Frames.Count; ++i) {
                pngWidth = Math.Max(pngWidth, decoder.Frames[i].PixelWidth);
                pngHeight += decoder.Frames[i].PixelHeight;
            }
            bitmap = new Bitmap(pngWidth, pngHeight, PixelFormat.Format16bppGrayScale);
            using (Graphics g = Graphics.FromImage(pngBitmap)) {
                int y = 0;
                for (int i = 0; i < decoder.Frames.Count; ++i) {
                    using (Bitmap frameBitMap = Worker.BitmapFromSource(decoder.Frames[i])) {
                        g.DrawImage(frameBitMap, 0, y);
                        y += frameBitMap.Height;
                    }
                }
            }
        }
        EncoderParameters eps = new EncoderParameters(1);
        eps.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 16L);
        pngBitmap.Save(
                pngFilename,
                Worker.GetEncoderInfo("image/png"),
                eps);
        return pngBitmap;
    }
}

TL;DR

Graphics.FromImage says: This method also throws an exception if the image has any of the following pixel formats: Undefined, DontCare, Format16bppArgb1555, Format16bppGrayScale. So, use another format or try another Image library (which is not built on top of GDI+, like Emgu CV)

的文档

详细

您遇到了 OutOfMemoryException,但这与内存不足无关。只是 how Windows GDI+ is working and how it's wrapped in .NET. We can see that Graphics.FromImage calls GDI+:

int status = SafeNativeMethods.Gdip.GdipGetImageGraphicsContext(new HandleRef(image, image.nativeImage), out gdipNativeGraphics);

if status is not OK, it will throw an exception

internal static Exception StatusException(int status)
{
    Debug.Assert(status != Ok, "Throwing an exception for an 'Ok' return code");

    switch (status)
    {
        case GenericError: return new ExternalException(SR.GetString(SR.GdiplusGenericError), E_FAIL);
        case InvalidParameter: return new ArgumentException(SR.GetString(SR.GdiplusInvalidParameter));
        case OutOfMemory: return new OutOfMemoryException(SR.GetString(SR.GdiplusOutOfMemory));
        case ObjectBusy: return new InvalidOperationException(SR.GetString(SR.GdiplusObjectBusy));
        .....
     }
}

您的场景中的状态等于 3,所以它抛出 OutOfMemoryException 的原因。

为了重现它,我只是尝试从 16x16 位图创建图像:

int w = 16;
int h = 16;
Bitmap bitmap = new Bitmap(w, h, PixelFormat.Format16bppGrayScale);
Graphics g = Graphics.FromImage(bitmap); // OutOfMemoryException here

我使用 windbg + SOSEX for .NET extension 进行分析,这里是我能看到的: