将 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
的完整列表!相反,只需在解码器上迭代帧即可获得目标大小(BitmapFrame
有 PixelWidth
)。用它来创建你的目标 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 进行分析,这里是我能看到的:
首先,我将 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
的完整列表!相反,只需在解码器上迭代帧即可获得目标大小(BitmapFrame
有 PixelWidth
)。用它来创建你的目标 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 进行分析,这里是我能看到的: