セピアフィルター

コンセプト

セピア色の画像とは何なのでしょうか?
セピアというのは辞書を引けばわかりますがイカ墨から作られる顔料の一種です。
要するに、セピア顔料で描かれたが如き色の画像ということですね。

ところで Windows Bitmap 形式の画像は RGB という光の3元素で表される画素から構成されています。
この絵をセピア色にするということはどういうことなのでしょうか?
世の中にはモノクロ画像というものがあります。
あれは絵の明るい部分を白色、絵の暗い部分を黒色に変換したものです。
ならばセピア色の画像というのは絵の明るさをセピアの濃さで表したものと考えて良いのではないでしょうか?

注: 納得できない人はとっととお帰りください(笑)

どのように実装すればよいか

Bitmap 画像の全ての画素(pixel)に対して以下の作業を行います。

注: 24bit カラーの画像と仮定しています

  1. RGB から輝度を取り出す。
  2. その明るさに対応するセピア色の RGB 値を得る。
  3. 得た RGB 値で画素の値を置き換える。

さて、輝度を取り出すにはどうすれば良いでしょうか?
実は世の中には RGB 以外にも色を表すための方法があります。 これを表色系(カラーモデル)といい、それらの中には輝度を持つものが幾つかあります。
そのなかでも今回は YUV カラーモデルを使って輝度を取り出しています。

yuv->rgb と rgb->yuv の変換式は以下の通りです。
yuv2rgb, rgb2yuv matrix image

YUV では y が輝度、u, v が色を表します。
よって全ての画素の u, v にセピア色の値を入れて変換すればセピア色の画像が出来あがりです。

YUV を使うと上の手順はこうなります。

  1. RGB から輝度 y を取り出す。
  2. 得た輝度 y とセピア色を表す色差 u, v を用いて YUV->RGB 変換をします。
  3. 得た RGB 値で画素の値を置き換える。

実装

上のやり方そのままに実装したものがこれです。 sepiaf01.pas
固定値の U=-0.091 と V=0.056 はこんなもんだろうと適当に決めた数値です。
気に食わない人は置き換えちゃってください。
適当に決めたといいつつやけに細かいなと思った人・・・気にしないでください(笑)
PentiumII 266PE MHz(Dixon 256K)で 640*480 画像を変換するのに、このソースでは600ミリ秒かかりました。

最適化

600ミリ秒は遅いので最適化します。

まずは浮動小数点を全部潰します。
これから先のことは知りませんが、現時点では多少の浮動小数点数から整数にする変換作業を含めても整数の方がまず速いです。
それを行ったのがこのソース。 sepiaf02.pas
PentiumII 266PE MHz(Dixon 256K)で640*480 画像を変換するのに、このソースでは150ミリ秒かかりました。

その次にいらない処理をカットします。

  for Y := 0 to Bmp.Height - 1 do
  begin
    P := Bmp.Scanline[Y];

    for X := 0 to Bmp.Width - 1 do
    begin
      RGBColor.B := P[X].rgbtBlue;
      RGBColor.G := P[X].rgbtGreen;
      RGBColor.R := P[X].rgbtRed;

      YUVColor := RGB2YUV(RGBColor);  ・・・(i)

      YUVColor.U := Trunc(-0.080 * 1024 * 255 + 0.5);  //-0.091
      YUVColor.V := Trunc(0.071 * 1024 * 255 + 0.5);   //0.056    ・・・(ii)

      RGBColor := YUV2RGB(YUVColor);  ・・・(iii)

      P[X] := NormalizeRGB(RGBColor);
    end;
  end;

(i) 輝度だけ得れば良いのに U, V まで計算している。
(ii) U, V に入れる値は不変値なので毎回計算して代入するのは不毛。(*1)
(iii) わざわざ変数に代入する必要性はない。

(*1) コンパイラによっては最適化でループの外に追い出しますが・・・。

以上を直すと以下のようになります。

  YUVColor.U := Trunc(-0.080 * 1024 * 255 + 0.5);  //-0.091
  YUVColor.V := Trunc(0.071 * 1024 * 255 + 0.5);   //0.056    ・・・(ii)

  for Y := 0 to Bmp.Height - 1 do
  begin
    P := Bmp.Scanline[Y];

    for X := 0 to Bmp.Width - 1 do
    begin
      with P[X] do
        YUVColor.Y := 306 * rgbtRed + 601 * rgbtGreen + 117 * rgbtBlue;  ・・・(i)

      P[X] := NormalizeRGB(YUV2RGB(YUVColor));  ・・・(iii)
    end;
  end;

ソースを汚くするのをいとわなければ YUV2RGB と NormalizeRGB を手作業で展開すると関数呼び出しコストを支払わなくて良くなり更に高速になります。
sepiaf03.pas PentiumII 266PE MHz(Dixon 256K)で 640*480 画像を変換するのに、このソースでは110ミリ秒かかりました。

しかし実はまだ無駄が残っています。
RGB の値は Y が決まれば一意に決まります。
それなのに毎回 U, V を用いて RGB 値を計算するのは無駄です。
よって前もって計算して配列に入れておけば良いわけです。

  YUVColor.U := Trunc(-0.080 * 1024 * 255 + 0.5);  //-0.091
  YUVColor.V := Trunc(0.071 * 1024 * 255 + 0.5);   //0.056
  
  for Y := 0 to 1024 do
  begin
    YUVColor.Y := Y * 255;  ・・・Y は 1024*255 も範囲を持たせる必要がないので間引いている
    ColorTable[Y] := NormalizeRGB(YUV2RGB(YUVColor));
  end;

  for Y := 0 to Bmp.Height - 1 do
  begin
    P := Bmp.Scanline[Y];

    for X := 0 to Bmp.Width - 1 do
    begin
      with P[X] do
        P[X] := ColorTable[(306 * rgbtRed + 601 * rgbtGreen + 117 * rgbtBlue) div 255];  ・・・(iv)
    end;
  end;
end;

このような手法をルックアップテーブルといいます。
sepiaf04.pas PentiumII 266PE MHz(Dixon 256K)で 640*480 画像を変換するのに、このソースでは55ミリ秒かかりました。

最終的に約11倍まで高速化できました。

もっと速い方法があるぜという方、教えて貰えたら幸いです。
(MMX, SSE, 3DNow! は却下です)

補足: (iv) の部分の div 255 を shr 8 にしたら変換所要時間が25ミリ秒になりました。
やっぱり割り算のコストは高いのね・・・。


修正

ここから上は 1999/12/18 までに書き上げられたものです。
今(2002/10/16)見直すと色々と問題があるので修正していきます。

>これから先のことは知りませんが、現時点では多少の浮動小数点数から整数にする変換作業を含めても整数の方がまず速いです。
Pentium 以降の CPU では乗算等では浮動小数点のほうが速いです。
固定小数点演算の方が速いのはキャストのコストがいらないことや、ルックアップテーブルが使える点でしょう。
>補足: (iv) の部分の div 255 を shr 8 にしたら変換所要時間が25ミリ秒になりました。
そもそも shr 8 は div 256 と等価なので、速いからといって差し替えれるものではないと思います。
実際にピクセル距離(*1)平均を取ったところ 1.3272 という大きな数値が出ました。
(*1) sqrt((p1.r - p2.r) ^ 2 + (p1.g - p2.g) ^ 2 + (p1.b - p2.b) ^ 2)

そもそも固定小数点のソースは小数の定数値ですら整数の定数値で置き換えられています。
これではぱっと見で精度が分らない上に、四捨五入がちゃんと行われているかも分りません。
実際に下にまとめてみましたが精度にいろいろと問題がありました。
そのため修正したソースを作っておきました。
sepiaf.pas

PentiumII 266MHz Celeron 450MHz 精度
sepia01 600ms 400ms -
sepia02 150ms 90ms 1.1587
sepia03 110ms 70ms 1.1587
sepia04(div) 55ms 30ms 0.6040
sepia04(shr) 25ms 20ms 1.3272
sepia05(Prec12) - 20ms 0.0458
sepia05(Prec14) - 30ms 0.0183
sepia05(Prec15) - 50ms 0.0063
sepia05(Prec16) - 80ms 0.0000

640x480, PentiumII 266PE MHz(Dixon 256K), Celeron 450 MHz(Mendocino Celeron 300A MHz のオーバークロック。


Return index page