我正在处理巨大的TIFF图像(灰度,8或16位,最高4 GB),可用作机器的高分辨率输入数据。每个图像需要旋转90度(顺时针)。输入TIFF可以是LZW或未压缩,输出可以是未压缩的。
到目前为止,我在Objective C中实现了自己的TIFF阅读器类(包括LZW解压缩),它能够处理大文件并在内存中进行一些缓存。目前,TIFF阅读器类用于图像内部的可视化和测量,并且表现非常好。
对于我的最新挑战,旋转TIFF,我需要一种新的方法,因为当前的实现非常慢。即使是“中等”尺寸的TIFF(30.000 x 4.000)也需要大约。旋转图像30分钟。此刻,我循环遍历所有像素并选择具有反转x和y坐标的像素,将所有像素放入缓冲区并在一行完成后将缓冲区写入磁盘。主要问题是从TIFF读取,因为数据是以条带形式组织的,并且不保证在文件内部线性分布(在LZW压缩条带的情况下,也没有线性也是如此)。
我对我的软件进行了分析,发现大部分时间花在复制内存块(memmove)上,并决定绕过我的阅读器类中的缓存进行轮换。现在整个过程快了大约5%,这不是太多,现在所有的时间都花在了fread()中。我假设至少我的缓存执行几乎与系统的fread()缓存一样好。
使用具有相同30.000 x 4.000文件的Image Magick的另一个测试仅需要大约10秒钟才能完成。 AFAIK Image Magick将整个文件读入内存,在内存中处理,然后写回磁盘。这可以很好地处理几百兆的图像数据。
我正在寻找的是某种“元优化”,就像处理像素数据的另一种方法一样。是否有另一种策略,而不是逐个交换像素(并且需要从远离彼此的文件位置读取)?我应该创建一些中间文件来加快这个过程吗?欢迎任何建议。
答案 0 :(得分:2)
好的,鉴于您必须进行像素修改,让我们来看看您的整体问题。 30000x4000像素的中间图像是用于8位灰度的120M图像数据和用于16位的240M图像数据。所以,如果你以这种方式查看数据,你需要问“30分钟是否合理?”为了进行90度旋转,你会在内存方面引发最坏情况的问题。您正在触摸单个列中的每个像素以填充一行。如果你按行工作,至少你不会加倍内存占用空间。
所以 - 120M像素意味着您正在进行120M读取和120M写入,或240M数据访问。这意味着您每秒处理大约66,667像素,我认为这太慢了。我认为你应该每秒处理至少 50万像素,可能更多。
如果这是我,我会运行我的分析工具,看看瓶颈在哪里并切断它们。
如果不知道您的确切结构并且不得不猜测,我会执行以下操作:
尝试为源图像使用一个连续的内存块
我希望看到像这样的旋转功能:
void RotateColumn(int column, char *sourceImage, int bytesPerRow, int bytesPerPixel, int height, char *destRow)
{
char *src = sourceImage + (bytesPerPixel * column);
if (bytesPerPixel == 1) {
for (int y=0; y < height; y++) {
*destRow++ = *src;
src += bytesPerRow;
}
}
else if (bytesPerPixel == 2) {
for (int y=0; y < height; y++) {
*destRow++ = *src;
*destRow++ = *(src + 1);
src += bytesPerRow;
// although I doubt it would be faster, you could try this:
// *destRow++ = *src++;
// *destRow++ = *src;
// src += bytesPerRow - 1;
}
}
else { /* error out */ }
}
我猜测循环内部将变成8条指令。在2GHz处理器上(假设每个指令名义上只有4个周期,这只是一个猜测),你应该能够在一秒钟内旋转6.25亿个像素。大致。
如果您不能连续,请一次处理多个目标扫描线。
如果源图像被分成块或者你有一个扫描线抽象的内存,你要做的是从源图像中获取一条扫描线,然后将几十列一次旋转到dest扫描线的缓冲区。 / p>
假设您有一种抽象访问扫描线的机制,其中您可以获取并释放和写入扫描线。
那么你要做的是弄清楚你愿意一次处理多少个源列,因为你的代码看起来像这样:
void RotateNColumns(Pixels &source, Pixels &dest, int startColumn, int nCols)
{
PixelRow &rows[nRows];
for (int i=0; i < nCols; i++)
rows[i] = dest.AcquireRow(i + startColumn);
for (int y=0; y < source.Height(); y++) {
PixelRow &srcRow = source.AcquireRow();
for (int i=0; i < nCols; i++) {
// CopyPixel(int srcX, PixelRow &destRow, int dstX, int nPixels);
sourceRow.CopyPixel(startColumn + i, rows[i], y, 1);
}
source.ReleaseRow(srcRow);
}
for (int i=0; i < nCols; i++)
dest.ReleaseAndWrite(rows[i]);
}
在这种情况下,如果在大型扫描线块中缓冲源像素,则不一定要对堆进行分段,并且可以选择将已解码的行刷新到磁盘。您一次处理n列,并且您的内存位置应该提高n倍。然后问题就是你的缓存有多贵。
可以通过并行处理解决问题吗?
老实说,我认为你的问题应该是IO绑定,而不是CPU绑定。我认为你的解码时间会占主导地位,但让我们假装它不是,因为笑容。
以这种方式思考 - 如果您一次读取整行的源图像,您可以将该解码的行抛到一个线程中,该线程将其写入目标图像的相应列。所以写你的解码器,使它有一个像OnRowDecoded(byte * row,int y,int width,int bytesPerPixel)的方法;然后你在解码时旋转。 OnRowDecoded()打包信息并将其交给拥有dest图像的线程,并将整个解码的行写入正确的dest列。当主线程忙于解码下一行时,该线程执行对dest的所有写入。可能工作线程将首先完成,但可能不会。
你需要让你的SetPixel()到目标是线程安全的,但除此之外,没有理由这应该是一个串行任务。实际上,如果您的源图像使用TIFF功能划分为波段或图块,您可以并且应该并行解码它们。
答案 1 :(得分:1)
如果查看TIFF规范,可以将标记添加到设置image orientation的图像IFD中。如果适当地设置此标记,则可以更改图像旋转,而无需对图像进行解码和重新编码。
然而 - 这是一个大然而 - 你应该知道,虽然它看起来很直接,如果在TIFF中重写IFD并不是微不足道的,那么处理生态系统中所有异常的TIFF就是明确的非平凡的,所以要小心你怎么做。