4

我正在处理巨大的 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() 缓存一样好。

使用 Image Magick 和相同的 30.000 x 4.000 文件进行的另一项测试仅用了大约 10 秒即可完成。AFAIK Image Magick 将整个文件读入内存,在内存中处理,然后写回磁盘。这适用于数百兆字节的图像数据。

我正在寻找的是某种“元优化”,例如另一种处理像素数据的方法。除了逐个交换像素(并且需要从彼此远离的文件位置读取)之外,还有另一种策略吗?我应该创建一些中间文件来加速这个过程吗?欢迎任何建议。

4

2 回答 2

3

好的,鉴于您必须进行像素修正,让我们看看您的整体问题。30000x4000 像素的中等图像是 120M 的 8 位灰度图像数据和 240M 的 16 位图像数据。因此,如果您以这种方式查看数据,您需要问“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 扫描线。

如果源图像被分成块或者你有内存的扫描线抽象,你所做的就是从源图像中获取一条扫描线,然后一次旋转几十列到目标扫描线的缓冲区中。

假设您有一种抽象访问扫描线的机制,您可以在其中获取、释放和写入扫描线。

然后你要做的是弄清楚你愿意一次处理多少个源列,因为你的代码看起来像这样:

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 将其划分为条带或图块的功能,您可以并且应该并行解码它们。

于 2012-11-13T18:53:21.023 回答
1

如果您查看 TIFF 规范,可以将标签添加到设置图像方向的图像 IFD 中。如果您适当地设置此标签,您可以更改图像旋转,而无需对图像进行解码和重新编码。

然而——这是一个很大的问题——你应该知道,虽然它看起来很简单,但如果在 TIFF 中重写 IFD 不是微不足道的,那么处理生态系统中所有异常的 TIFF 绝对不是微不足道的,所以要小心你如何去关于它。

于 2012-11-13T14:24:23.683 回答