4

巨大な TIFF 画像 (グレースケール、8 ビットまたは 16 ビット、最大 4 GB) を処理して、マシンの高解像度入力データとして使用しています。各画像を 90 度 (時計回り) 回転させる必要があります。入力 TIFF は LZW または非圧縮の場合があり、出力は非圧縮の場合があります。

これまでのところ、Objective C (LZW 圧縮解除を含む) で独自の TIFF リーダー クラスを実装しました。これは、巨大なファイルを処理でき、メモリ内でのキャッシュも行います。現在、TIFF リーダー クラスは、画像内の視覚化と測定に使用されており、非常に優れたパフォーマンスを発揮します。

私の最新の課題である TIFF のローテーションでは、現在の実装が非常に遅いため、新しいアプローチが必要です。「中」サイズの TIFF (30.000 x 4.000) の場合でも、約 1 秒かかります。画像を回転させるのに30分。現時点では、すべてのピクセルをループして、x 座標と y 座標が逆になっているピクセルを選択し、それらすべてをバッファーに入れ、1 行が完了するとすぐにバッファーをディスクに書き込みます。主な問題は TIFF からの読み取りです。これは、データがストリップに編成されており、ファイル内で線形に分散されていることが保証されていないためです (LZW 圧縮ストリップの場合、何も線形ではありません)。

ソフトウェアのプロファイリングを行ったところ、ほとんどの時間がメモリ ブロックのコピー (memmove) に費やされていることがわかり、ローテーションのためにリーダー クラス内のキャッシュをバイパスすることにしました。現在、プロセス全体が約 5% 高速化されていますが、これは大したことではなく、すべての時間が fread() 内で費やされています。少なくとも私のキャッシュは、システムの fread() キャッシュとほぼ同じように機能すると思います。

同じ 30.000 x 4.000 ファイルで Image Magick を使用した別のテストでは、完了までに約 10 秒しかかかりませんでした。AFAIK Image Magick はファイル全体をメモリに読み込み、メモリで処理してからディスクに書き戻します。これは、数百メガバイトの画像データまでうまく機能します。

私が探しているのは、ピクセルデータを処理するための別のアプローチのような、ある種の「メタ最適化」です。ピクセルを 1 つずつ交換する (そして、互いに遠く離れたファイルの場所から読み取る必要がある) 以外の戦略はありますか? プロセスを高速化するために中間ファイルを作成する必要がありますか? どんな提案でも大歓迎です。

4

2 に答える 2

3

ピクセル変更を行う必要があることを考えると、全体的な問題を見てみましょう。30000x4000 ピクセルの中間イメージは、8 ビット グレーの場合は 120M のイメージ データ、16 ビットの場合は 240M のイメージ データです。このようにデータを見ているのであれば、「30分は妥当か?」と問う必要があります。90 度の回転を行うには、メモリに関して最悪のケースの問題が発生します。1 つの行を埋めるために、1 つの列のすべてのピクセルに触れています。行単位で作業する場合、少なくともメモリ フットプリントが 2 倍になることはありません。

つまり、120M のピクセルは、120M の読み取りと 120M の書き込み、または 240M のデータ アクセスを行っていることを意味します。これは、1 秒あたり約 66,667 ピクセルを処理していることを意味します。これは遅すぎると思います。毎秒少なくとも50 万ピクセル、おそらくそれ以上を処理する必要があると思います。

これが私だったら、プロファイリング ツールを実行して、ボトルネックがどこにあるかを確認し、それらを取り除きます。

正確な構造を知らずに推測する必要がなければ、次のことを行います。

ソース イメージに 1 つの連続したメモリ ブロックを使用しようとする

次のような回転関数を見たいと思います:

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 サイクルとしましょう。これは単なる推測です) では、1 秒間に 6 億 2,500 万ピクセルを回転できるはずです。だいたい。

連続できない場合は、一度に複数の dest スキャンラインで作業してください。

ソース画像がブロックに分割されている場合、またはメモリのスキャンライン抽象化がある場合、ソース画像からスキャンラインを取得し、たとえば数十列を一度に回転させて、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 倍向上するはずです。次に、キャッシングがどれだけ高価かという問題になります。

この問題は並列処理で解決できますか?

正直なところ、あなたの問題はCPUバウンドではなく、IOバウンドであるべきだと思います。あなたのデコード時間が支配的だと思いますが、そうではないふりをしましょう。

このように考えてみてください。一度にソース イメージの行全体を読み取ると、そのデコードされた行を、宛先イメージの適切な列に書き込むスレッドに投げることができます。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 に答える