セピア色の画像とは何なのでしょうか?
セピアというのは辞書を引けばわかりますがイカ墨から作られる顔料の一種です。
要するに、セピア顔料で描かれたが如き色の画像ということですね。
ところで Windows Bitmap 形式の画像は RGB という光の3元素で表される画素から構成されています。
この絵をセピア色にするということはどういうことなのでしょうか?
世の中にはモノクロ画像というものがあります。
あれは絵の明るい部分を白色、絵の暗い部分を黒色に変換したものです。
ならばセピア色の画像というのは絵の明るさをセピアの濃さで表したものと考えて良いのではないでしょうか?
注: 納得できない人はとっととお帰りください(笑)
Bitmap 画像の全ての画素(pixel)に対して以下の作業を行います。
注: 24bit カラーの画像と仮定しています
さて、輝度を取り出すにはどうすれば良いでしょうか?
実は世の中には RGB 以外にも色を表すための方法があります。
これを表色系(カラーモデル)といい、それらの中には輝度を持つものが幾つかあります。
そのなかでも今回は YUV カラーモデルを使って輝度を取り出しています。
yuv->rgb と rgb->yuv の変換式は以下の通りです。
YUV では y が輝度、u, v が色を表します。
よって全ての画素の u, v にセピア色の値を入れて変換すればセピア色の画像が出来あがりです。
YUV を使うと上の手順はこうなります。
上のやり方そのままに実装したものがこれです。 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)見直すと色々と問題があるので修正していきます。
>これから先のことは知りませんが、現時点では多少の浮動小数点数から整数にする変換作業を含めても整数の方がまず速いです。
>補足: (iv) の部分の div 255 を shr 8 にしたら変換所要時間が25ミリ秒になりました。
そもそも固定小数点のソースは小数の定数値ですら整数の定数値で置き換えられています。
これではぱっと見で精度が分らない上に、四捨五入がちゃんと行われているかも分りません。
実際に下にまとめてみましたが精度にいろいろと問題がありました。
そのため修正したソースを作っておきました。
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 のオーバークロック。