画像のリサイズを行うときに陥りがちな罠


罠シリーズ2回目。(べつにシリーズ化はしないけどね。たまたま)

まず、リサイズについておさらい。

リサイズとは、その名のとおり画像データの拡大または縮小のこと。
アルゴリズムとしてはどうなっているかというと、
近傍画素の情報から、画素を「補間」するというもの。

は?
(いや、ぼくはわかってますよ(汗)

わかりやすく「拡大」で考えると、
拡大後の画像データは、拡大前の画像データより画素が増えるわけです。

この「増えた」画素は、本来、拡大前の画像データには存在しない画素。
存在しない画素を、どこかから補うのが「補間」。

ではどこから補うのかというと、近傍画素(周辺の画素)から補うのです。
近傍画素も1つだけ見るよりたくさん見たほうがより結果の精度は上がります。
その分、処理時間はかかります。トレードオフです。

今回は、16点の近傍画素から補間を行う「バイキュービック」を使ったリサイズで考えてみます。

では、まず以下の画像を2倍にリサイズしてみます。
(表示サイズは、リサイズ前と後とで同じにしています)

リサイズ後の方が、エッジがぼけて見えますが、それは問題ないです。
近傍画素から補間を行うということは、平均化しているのとほぼ変わらないので、補間された画素は近傍画素の平均値の画素になると思ってください。
むしろ、エッジが平均化されるので、ジャギ(極端な画素差)がなくなり、きれいに見えることを狙ったりするくらいです。

問題は、これからです。

いまリサイズした画像はアルファチャンネルを持つ32ビット画像です。
これを、アルファチャンネルを透過情報として、市松レイヤーに貼り付けてみます。
結果が以下です。

わかりずらいのでリサイズ後画像の表示をズームしてみます。

エッジになにやら青いスジが見えますよね。青色は、アルファチャンネルのマスク外の色でした。
これが罠です。

つまり、本来ならマスク内に現れるはずのない色なのに、リサイズ時に補間されたことで、マスク外の色がマスク内に侵食したのです。

なので、32ビット透過画像をリサイズするときは、
近傍画素を見るとき、その画素のアルファチャンネルを確認し、マスク外であるなら、その近傍画素はパスするという処理を入れてやる必要があります。
もしくは・・・、まあここではいいや。暇があったらまた書きます。

結果が以下です。

しかし、RGBチャンネルは

のようにリサイズ前画像から大きく変わってしまいます。
32ビット画像でも透過させる用途で使わないなら、上で述べたことは気にする必要はないです。

(具体的にバイキュービックのコードを例にどう修正すればいいかをのせようとおもったけど、
文が多くなっちゃったので割愛・・・)

画像のアルファブレンドを行うときに陥りがちな罠


まず、アルファブレンドについておさらい。

アルファブレンドは以下の式で行う。
dst = a * src1 + (1 - a) * src2
    a:重み[0..1]

2枚の入力画像に対応する画素値src1とsrc2に対する単純な重み付き平均の式だが、
重みaをsrc1のアルファ値にすることで、src1を前景画像、src2を背景画像とする合成処理を行うことができる。(dst=src2)


 

unsigned char Fg[imageSize];
unsigned char Bg[imageSize];
for ( int i = 0; i < imageSize; i += 4 )
{
    unsigned char a = Fg[i + 3];   // αチャンネル.
    Bg[i + 0] = (Fg[i + 0] * a + Bg[i + 0] * (255 - a)) / 255;   // Bチャンネル.
    Bg[i + 1] = (Fg[i + 1] * a + Bg[i + 1] * (255 - a)) / 255;   // Gチャンネル.
    Bg[i + 2] = (Fg[i + 2] * a + Bg[i + 2] * (255 - a)) / 255;   // Rチャンネル.
}

ふつうじゃこんな書き方はしない(というよりありえない)けど、ようはこんなかんじ。

ここで、
こういうシチュエーションがたまにあったりする。
1.前景画像が複数ある。
2.かつ、前景画像は変わらないが背景画像を場合応じて変えたい。

べつに上のような状況でも、背景画像にどんどんアルファブレンディングしていったらいいのだけども、
背景画像が変わるたびにそれらを都度行うのはあまりにも効率が悪い。
(サイズが大きかったり、数が多かったりするともはや無視できるコストじゃなくなる)

というわけで、背景画像と同じサイズのレイヤーを1つ用意しておき、
複数の前景画像の合成をレイヤーに対して先に行なっておくことで、
あとは、背景画像を変えるタイミングではレイヤーを1回だけ合成すればすむようになる。

じゃあそれでいいじゃん。
しかしそうは問屋が下ろさない。

実際やってみましょう。


の3つの前景画像を用意。
(マスクにあたるアルファ値は0xA4)

まず、レイヤーを介さずに通常通りにアルファブレンドを行った場合。


 
次に、レイヤーを介した場合。


 
結果が違うことがおわかりでしょうか。

なにが悪いのかは明白で、レイヤーへの合成をアルファブレンドで行なっているから。
そのため、レイヤーに合成した時点で、レイヤーの色とアルファブレンディングされてしまう。
(今回はレイヤーを(0xff, 0xff, 0xff)にしてたので、白っぽくなっちゃったわけ)

やりたいことは、
・レイヤーとのアルファブレンドはしない。
・しかし、レイヤー上ですでに合成された画素とのアルファブレンドは行う。
の2点。

これらを考慮したのが、以下のコード。

unsigned char Fg[imageSize];
unsigned char Bg[imageSize];
for ( int i = 0; i < imageSize; i += 4 )
{
    unsigned char fa = Fg[i + 3];   // 前景画像のαチャンネル.
    unsigned char ba = Bg[i + 3];   // 背景画像のαチャンネル.
    unsigned char a = (fa + ba) - fa * ba / 255;    // 合成後のαチャンネル.

    Bg[i + 0] = (Fg[i + 0] * fa + Bg[i + 0] * (255 - fa) * ba / 255) / a;   // Bチャンネル.
    Bg[i + 1] = (Fg[i + 1] * fa + Bg[i + 1] * (255 - fa) * ba / 255) / a;   // Gチャンネル.
    Bg[i + 2] = (Fg[i + 2] * fa + Bg[i + 2] * (255 - fa) * ba / 255) / a;   // Rチャンネル.
}

 
重み付けを、前景画像の重みと背景画像(この場合はレイヤー)の重みの両方を加味して行なっている。

レイヤーへの合成を、通常のアルファブレンドではなく、上の合成で行うことで、
最終背景画像とレイヤーとの合成をアルファブレンドで行なっても、結果が異なることがなくなる。


ただ、上のコードは不具合と問題点がある。
不具合の方は一目瞭然だけど。
あともう一つは最終結果。かなり簡略化したので、正確にはレイヤーを介したときと
そうでないときとで結果が完全に一致しないときがある。
(画素によって、1程度の誤差が出る)

けどまあ、こだわりだすとここに書けるレベルの話じゃなくなってくるし。
(量が多くなってめんどくさいって意味でね)

構造化『並列』プログラミング


構造化プログラミング。
といえば、順次分岐ループのパターンからなる、シリアルプログラミングにおける基本スタイルですね。(これらだけしか使わなくてもシリアルプログラムは書けますっていうね)
C++にはあらかじめこれらのパターンを簡単に使えるようにシンタックスが用意されています。
分岐はifやswitch、ループはwhile、for、doといったかんじで。

もちろんこれらのパターンは並列プログラミングでも使えます。
しかし並列プログラミングにおいては、これらのパターンだけで実装を行うことはほぼ不可能です。
そこで、並列プログラミングにおけるパターンとなる構造が考えられてきました。
これらを使うことで、よりパワフルかつ簡潔に並列プログラムのコードを書けるようになります。


1.マップ
もっとも基本的な並列パターンです。
まず、コードを見てください。

template <class T>
void map(T out[], const T in[], size_t size)
{
    for ( size_t i = 0; i < size; i++ )
    {
        out[i] = in[i];
    }
}

inの要素をoutにコピーしています。
これは、入力と出力の要素が1対1であり、各要素が影響し合いません。
よって、0からsize-1まで律儀に順を追う必要がなく、同時に実行しても問題ありません。
イメージとしては、

といったかんじです。
このパターンをマップといいます。

C++には、並列パターンがシンタックスとして用意されていません。
よって、これらの並列パターンを自分であらかじめ作っておくことといいと思います。
一度作ってしまえば、可読性のある並列プログラミングをいつでも書く用意ができたと言っていいでしょう。(ライブラリアンになるのだ)

言語拡張しているコンパイラやライブラリを使うのも手です。
今回はTBBを使ってみます。

template <class T>
void map(T out[], const T in[], size_t size)
{
    tbb::parallel_for(
        tbb::blocked_range<T>(0, size),
        [&](const tbb::blocked_range<T>& range)
        {
            for ( auto i = range.begin(); i != range.end(); ++i )
            {
                out[i] = in[i];
            }
        },  
        tbb::auto_partitioner()
    );
}

上記のようにparallel_forを使えば、マップを実現できます。


2.リダクション
これもまずコードを見てください。

template <class T>
T reductione(const T in[], size_t size)
{
    T sum = 0;
    for ( size_t i = 0; i < size; i++ )
    {
        sum += in[i];
    }
    return sum;
}

inの要素をsumに足しこんでいっています。
sumは加算前の状態を保持している必要があるので、マップでは無理ですね。
しかし、加算はどの順序で足し合わせても結果が同じになるので同時に実行することは可能です。
イメージとしては、

といったかんじです。
このパターンをリダクションといいます。

TBBを使った例を挙げておきます。

template <class T>
T reductione(const T in[], size_t size)
{
    return tbb::parallel_reduce(
        tbb::blocked_range<T>(0, size),
        T(0),
        [=](const tbb::blocked_range<T>& range, T sum) -> T
        {
            for ( auto i = range.begin(); i != range.end(); ++i )
            {
                sum += in[i];
            }
            return sum;
        },
        std::plus<T>(),
        tbb::auto_partitioner()
    );
}

上記のようにparallel_reduceを使えば、リダクションを実現できます。


3.スキャン
これもまずコードを見てください。

template <class T>
T scan(T out[], const T in[], size_t size)
{
    T sum = 0;
    for ( size_t i = 0; i < size; i++ )
    {
        out[i] = sum += in[i];
    }
    return sum;
}

マップとリダクションのフュージョンです。
これも考え方が合わさっただけで理屈は前の2つと同じですので、同時に実行することは可能です。
イメージとしては、

といったかんじです。
このパターンをスキャン(一次元リカレンス)といいます。

TBBを使った例を挙げておきます。

template <class T>
T scan(T out[], const T in[], size_t size)
{
    scan_body<T> body(out, in);

    tbb::parallel_scan(
        tbb::blocked_range<T>(0, size),
        body,
        tbb::auto_partitioner()
    );

    return body.sum;
}

上記のようにparallel_scanを使えば、スキャンを実現できます。
注意する点は、マップにあたる部分の処理とリダクションにあたる部分の処理を分ける必要があるため、関数オペレータ以外にコールバックされる関数を定義します。
よって、今までのようにラムダ式で渡せません。
以下のようなメンバ関数を持つクラスオブジェクトをファンクタとして渡しています。

// body for scan
template <class T>
struct scan_body
{
    T sum;
    T* out;
    const T* in;

    scan_body(T* out, const T* in)
        : sum(0)
        , out(out)
        , in(in)
    {
    }

    scan_body(const scan_body& body, tbb::split)
        : sum(0)
        , out(body.out)
        , in(body.in)
    {
    }

    template <typename Tag>
    void operator()(const tbb::blocked_range<T>& range, Tag)
    {
        for ( auto i = range.begin(); i != range.end(); ++i )
        {
            out[i] = sum += in[i];
        }
    }

    void reverse_join(const scan_body& body)
    {
        sum += body.sum;
    }

    void assign(const scan_body& body)
    {
        sum = body.sum;
    }
};

4.Fork-Join
これもまずコードを見てください。

T in[100];                // 1 input
T out1[100], out2[100];   // 2 output

map(out1, in, sizeof(in) / sizeof(T));
map(out2, in, sizeof(in) / sizeof(T));

関数mapは、「1.マップ」で作った関数だと思ってください。
ここでは関数自体にはあまり意味はありません。ようは、なにかの処理を複数行っているということです。
ここでの2つの処理はお互いに影響を及ぼしません。つまり同時に実行できます。
イメージとしては、

といったかんじです。
このパターンをFork-Joinといいます。
並列処理として一番イメージしやすいかもしれません。

TBBを使った例を挙げておきます。

T in[100];                // 1 input
T out1[100], out2[100];   // 2 output

tbb::parallel_invoke(
    [&](){ map(out1, in, sizeof(in) / sizeof(T)); },
    [&](){ map(out2, in, sizeof(in) / sizeof(T)); },
);

上記のようにparallel_invokeを使えば、Fork-Joinを実現できます。
ただし、並列実行する処理に前後関係がある場合にはこのパターンは使えません。
(たとえば、out1への出力を終えてからout2への出力を行う必要がある場合とか)

また、task_groupを使って以下のようにも書けます。(おまけ)

tbb::task_group group1;
tbb::task_group group2;

group1.run(
    [&]()
    {
        map(out1, in, sizeof(in) / sizeof(T));
    }
);

group2.run(
    [&]()
    {
        map(out2, in, sizeof(in) / sizeof(T));
    }
);

group1.wait();
group2.wait();

5.パイプライン
これもまずコードを見てください。

for ( size_t i = 0; i < size; i++ )
{
    T data = read(i);   // どっかから読み込む.

    treble(data);         // なにか処理をかける.

    write(data, i);       // どっかに書き込む.
}

読み込み/3倍処理/書き込みの3つの処理を繰り返しています。
ここで重要な点は2つです。
1つ目は、ループの中の処理(ステージ)の順番です。
読み込み→3倍処理→書き込みの順番は絶対に守らなければ、正しい結果が得られません。しかし、前後の周回のステージとの順番は気にする必要はありません。(10回目の書き込みの前に、11回目の3倍処理が行われていても問題ないよ)
2つ目は、インクリメンタルなデータがあるということ。(現在の状態を保持して次に渡していくってニュアンスでインクリメンタルってゆってまふ)
ここでのインクリメンタルなデータは i です。インクリメンタルなデータがあることで、周回を追い越すことはできません。(10回目の周回に入っていないのに、11回目の周回の処理は行えないよ)
これらのことに注意して並列化を行う必要があります。
イメージとしては、

といったかんじです。
このパターンをパイプラインといいます。
上の図では、インクリメンタルなデータは1つですが、もちろん1つとは限りません。

TBBを使った例を挙げておきます。

size_t i = 0;   // incremental data

// body for read
auto read_body = [&](tbb::flow_control& fc) -> Data<T> {
    if ( i >= size )
    {
        fc.stop();     // terminate roop
        return Data<T>(-1, T());
    }
    
    Data<T> data = Data<T>(i, read(i));   // read data

    i++;
	
    return data;
};

// body for treble
auto treble_body = [](Data<T> data) -> Data<T> {
    treble(data.value);
    return data;
};

// body for write
auto write_body = [](Data<T> data) {
    write(data.value, data.index);   // write data
};

tbb::parallel_pipeline(
    16, // max_number_of_live_tokens
    
    tbb::make_filter<void, Data<T>>(
        tbb::filter::serial_in_order,
        read_bosy
    ) &
    tbb::make_filter<Data<T>, Data<T>>(
        tbb::filter::parallel,
        treble_body
    ) &
    tbb::make_filter<Data<T>, void>(
        tbb::filter::serial_in_order,
        write_body
    )
);
// class of data
template <class T>
struct Data
{
    size_t index;
    T value;

    Data(size_t index, T value)
        : index(index)
        , value(value)
    {
    }
};

上記のようにparallel_pipelineを使えば、パイプラインを実現できます。
(少しわざとらしいコードにしすぎて、逆にわかりにくくなったかも・・・)
make_filterのテンプレートパラメータに、body(並列化する関数オブジェクト)の引数の型と戻り値の型を指定します。1つ目の引数は、並列化の動作を指定します。上の例では、読み込みと書き込みはserial_in_orderを指定して並列化されないようにしています。


以上、5つの並列パターンを紹介しましたが、並列パターンはまだ存在します。(多いんだよ・・・)
並列パターンは、その用途を具体的にイメージしやすいものから、どういった場面で使うのかよくわからない複雑なものまで多様です。
マップ → 画像処理のトーンカーブ処理。レイトレーシング。など
リダクション → 行列演算。など
スキャン → 乱数生成。など
パイプライン → データ圧縮。など

自分のよく使う処理に適用できる並列パターンは目を通しておいて損はないかと思います。

構造化並列プログラミングについてより詳しく知りたい人は、この本がおすすめです。
Structured Parallel Programming: Patterns for Efficient Computation
Michael McCool (著), James Reinders (著), Arch Robison (著)

画像データへの簡易文字描画

デバッグ時に画像データにお手軽に文字情報を出力したいときなどが
たまにあったりなかったり。

ちゃっちゃと出力したときは、GDI+を使ってます。

int channel;
int width;
int height;
unsigned char* data = new unsigned char[width * height * channel];

////////////////////////////////////////////////////
// dataに文字描画.
// int x [i] 左上のx座標.
// int y [i] 左上のy座標.
// const char* string [i] 文字列.
// const char* fontName [i] フォント名.
// unsigned char r [i] フォント色R.
// unsigned char g [i] フォント色G.
// unsigned char b [i] フォント色B.
// int fontSize [i] フォントサイズ.(pt)

BITMAPINFO bmi = { sizeof(BITMAPINFOHEADER), width, height, 1, channel * 8 };

Gdiplus::Bitmap bmp*1;
wchar_t* wstring = new(std::nothrow) wchar_t[strlen(string) + 1];
MultiByteToWideChar(CP_ACP, 0, string, strlen(string) + 1, wstring, (strlen(string) + 1) * 2);
wchar_t* wfontName = new(std::nothrow) wchar_t[strlen(fontName) + 1];
MultiByteToWideChar(CP_ACP, 0, fontName, strlen(fontName) + 1, wfontName, (strlen(fontName) + 1) * 2);

Gdiplus::RectF rect(x, y, width - x - 1, height - y - 1);
Gdiplus::Font font(wfontName, fontSize);
Gdiplus::SolidBrush brush(Gdiplus::Color(r, g, b));
Gdiplus::StringFormat format;
graphics.DrawString(wstring, -1, &font, rect, &format, &brush);

if ( wstring ) delete wstring;
if ( wfontName ) delete
wfontName;

みたいなかんじ。

ワイド文字列バッファ確保を、new(std::nothrow) にしてるのは、
デバッグコードが例外投げられても困るからです。
wstring や wfontName が確保失敗でNULLでも、文字が描画されないだけで
その先の処理は進むし。

フォントサイズは、pt指定だけど、pixel指定にしたいなら、
fontSize = fontSize * dpi / 72;
とかにしたらいいんじゃないかな。
(このへんよくしらないです)

円描画なら、

////////////////////////////////////////////////////
// dataに楕円描画.
// int x [i] 左上のx座標.
// int y [i] 左上のy座標.
// int w [i] 横幅.
// int h [i] 縦幅.
// unsigned char r [i] フォント色R.
// unsigned char g [i] フォント色G.
// unsigned char b [i] フォント色B.
// int bold [i] 太さ.

BITMAPINFO bmi = { sizeof(BITMAPINFOHEADER), width, height, 1, channel * 8 };

Gdiplus::Bitmap bmp*2, bold);
graphics.DrawEllipse(&pen, x, y, w, h);

でいけるかな。

あと、GDI+を使う準備として、

#include
#pragma comment(lib, "gdiplus.lib")

struct CGdiplus
{
Gdiplus::GdiplusStartupInput input;
unsigned long token;

CGdiplus() { Gdiplus::GdiplusStartup(&token, &input, NULL); }
~CGdiplus() { Gdiplus::GdiplusShutdown(token); }
};

{
CGdiplus gdiplus;

// 処理.
}

てしとけばOK。

*1:PBITMAPINFO)(&bmi), data); Gdiplus::Graphics graphics(&bmp); graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); // アンチエイリアス. // GdiPlus::DrawStringが取りうる文字列の型がWCHAR* なので、 // char* → wchar_t* に変換. std::locale::global(std::locale("japanese"

*2:PBITMAPINFO)(&bmi), data); Gdiplus::Graphics graphics(&bmp); graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); // アンチエイリアス. Gdiplus::Pen pen(Gdiplus::Color(r, g, b

画像処理の高速化・番外編

昨日のEffectiveC++読書会#4で、
動的に確保する配列データもdeleteを使わんですむように、vector使えばいいじゃん。
vector使ってもパフォーマンスは変わらないよ。という話があがったので

それじゃってことで、やってみました。

int channel;
int width;
int height;
std::vector data(width * height * channel);

for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
for ( int c = 0; c < channel; c++ )
{
data[y * width * channel + x * channel + c] = F(data[y * width * channel + x * channel + c]);
}
}
}

ちなみに、F(x)は、明るさ・コントラスト調整のトーンカーブ関数です。
画像データサイズは、1252x1252の32bitの、約6.2MB。

結果。
2612ms!! おっそ

次に、イテレータで回してみました。

std::vector::iterator it = data.begin();

for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
for ( int c = 0; c < channel; c++, ++it )
{
*it = F(*it);
}
}
}

893ms。 うん、さっきよりはまし。

ちなみに、これを配列データで行った場合、
unsigned char* data = new unsigned char[width * height * channel];

30ms。

計測は、あくまでデータアクセスの箇所(for文三重ループのところ)のみに対して行いました。

てことで、vectorはゼロオーバーヘッドではないってことでいいのかなぁ。

Boost.勉強会 #10 のディスカッションのまとめメモ

テーブルキーパーになったので、まとめときます。
メモは写メしたけど落書きすぎて読めない。

初期テーマは、「よく使うBoostライブラリを出し合おう」でしたが、
なんだかしらないうちに、C++1yだのC++2xだの話が広がっていったかんじです。

まず、よく使うBoostライブラリは、
・Thread
・Asio
・Spirit
・Lambda
Phoenix
・Any
・Range
・PP
が上がりました。

Boost.Thread
C++11のstd::threadでも代用できるよね。
・けど、std::threadは、現在対応しているコンパイラgcc、clang、VC2012と限られるね。
・std::threadはインタラクションポイントがない。Boost.Threadにはある。
・moveに対応している。Boost.Moveではない。
・boost 1.55 から、V3になる。1.50でもV3の機能を使おうと思えば使える。

Boost.Asio
・とにかくサンプルがわかりにくい。
・リファレンスがわかりやすいので、それを見よう。
・魔導書vol.2で、牛さんが記事を書いてるのでそれ見ようね。

Boost.Spirit
・パーサジェネレータとして優秀。
C++の型チェックがきく。
・Boost.Asio と逆でこっちはリファレンスがわかりにくい。
C++11のregexよりこっちの方がいいかも。

Boost.Lambda
C++11のラムダ式で最早いいかも。

ここらへんで、BoostとC++11の機能の差分リストがほしいねという声が。

Boost.Phoenix
コンパイル時間がブーストする☆

Boost.Any
・(とくに話はしなかった)

Boost.Range
・OvenやRangeExtentionなど、ほかにいいライブラリがあるよ。

Boost.PP
・(とりあえず上げとけ的な流れで出てきた)

そのほかに、SandBoxから、
・Boost.Backtrace
・Boost.Cfunction
が上げられました。

ここで、なぜ、これらが正式に採用されないのかという話になり、
便利なものでもなかなか追加できないんだよ、という話に。
Boost.Backtraceは、Windows x86Linux上でしか動作できないなど
コンパイラや環境に依存する機能は、採用されにくい。
同じことが、今後のC++の規格にも言える。
C++の規格の場合はなおのことで、標準仕様として追加してしまうと
あとで削ることができない。


次に、C++11で追加された機能をピックアップしようという話。
・auto
ラムダ式
・move
regex
・type traits
・tuple
・random
・bind
・function
・chrono
・nullptr → C++03でもほしいよね。
・初期化子リスト
・文字列リテラル(u8とか)
・UDL

強化された機能。
・スマートポインタ
・for_each
・SFINAE
・hash_map


最後に、次期規格でほしい機能。
・mpl
GC
・concept
C++1yに入って欲しいけど、バグフィックスだけで、
実際入るとしたら、C++2xかなー。という話。


初期テーマが単純すぎた分、後半話が広がりすぎたような。

画像処理の高速化1

何回かに分けて画像処理の高速化について書こうと思います。

画像処理の基本は、ピクセル走査。
素直に書くと、

int channel;
int width;
int height;
unsigned char* data = new unsigned char[width * height * channel];

for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
for ( int c = 0; c < channel; c++ )
{
data[y * width * channel + x * channel + c] = F(data[y * width * channel + x * channel + c]);
}
}
}

ってなると思う。
ただ、この書き方はパディングをまったく考慮していません。
パディングっていうのは、画像サイズの幅を4バイト境界にアラインするためのゴミデータのこと。

ここで、簡単に高速化できそうなポイントとして、
1.for文のネストを減らす。
2.配列アクセスじゃなくてポインタアクセスにする。
がすぐに思いつく。

まず、1。
for ( int i = 0; i < width * height * channel; i++ )
{
data[i] = F(data[i]);
}

すっごいシンプル。
けど、この書き方は空間フィルタ(周辺画素も計算に必要)だと逆に見にくくなる。
なので、外側の2重ループは残すことが多い。

次に、2。
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++, data += channel )
{
for ( int c = 0; c < channel; c++ )
{
*(data + c) = F(*(data + c));
}
}
}

これは意外にそこまで速くならない。
配列アクセスでも連続した配列データなら、シーケンシャルアクセスとして
最適化してくれるみたい。

けど、最適化してくれないパターンがこれ。
boost::gil::bgra8_view_t data;

for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
for ( int c = 0; c < channel; c++ )
{
data(x, y)[c] = F(data(x, y)[c]);
}
}
}

Boost.GIL。
データアクセスにテンプレート使っちゃってるもんだから、
シーケンシャルに最適化されないのです。

Boost.GILは、データの抽象化に特化しすぎて、速度が完全に犠牲になっちゃってる残念さがある。

画像処理にそこまで速度を求めない場合や、画像データのサイズが小さい場合は気にならないのかもしれないけど、
僕が普段扱う画像データが、数十MBとかが当たり前なんで、遅いというのは
無視できないことなのです。

んじゃ画像処理にテンプレートは向かないのかというと、全くそういうことではない。要は使いどころですね。
(この話はまた別の機会に)

ソースコード色付けとかしたいけどやりかたわかんねー><