前の章で、ビットマップデータの操作を2次元配列で行いたい、ということを書きました。このことを考えたのは、ビットマップデータを回転させるプログラムを作っている時でした。
前の例題の様に画像全体にエフェクトを掛けるような場合は単に上から下、左から右に1ピクセルずつ操作していけばいいことですが、画像の回転となると違ってきます。
例えば左に90度回転するには、座標 (x, y) にあるピクセルを (y,
画像の高さ - x) という位置に移動する必要があります。
これをScanLineでやるには、ScanLine(y)読んだ元のデータとScanLine(画像の高さ
- x) で読んだ転送先のデータを交換することになります。具体的には、
Pnt1 := ScanLine(y);
Pnt2 := ScanLine(Height - x);
Tmp := Pnt2[y];
Pnt2[y] := Pnt1[x];
Pnt1[x] := Tmp;
という具合です。これを2次元配列を使って
Tmp := Data2[y, Height - x];
Data2[y, Height - x] := Data1[x, y];
Data1[x, y] := Tmp;
とすれば、すっきりとし、前のコードに比べて明らかに(X座標, Y座標)で何らかの操作が行われているのが分かります。
これを実現するために、Bitmap構造体にアクセスするとかあれこれ考えてみたのですが、その時は解決法が見つからず、その時はあきらめていました。
ところが、今回一連のサンプルを作っていく中で偶然Delphi4で、動的配列が使えることを知りました。
(実は、Delphiヘルプのトップ「Delphi 4の新機能」に書かれていることなのですが、見過ごしていて、別の項目を調べている時にこのことを知ったのです...^^;)。
Delphi4では、次のように動的配列の定義ができるようになりました。
A: array of Integer;
静的配列の宣言
A: array[1..100] of Integer;
にある、添字の部分を省略することで動的配列として定義できるのです。
2次元の配列の場合は、
A: array of array of Integer;
と、arrayを重ねて書きます。
宣言だけではこの変数を使うことはできません。配列の大きさを手続きの中で決めてやる必要があります。
SetLength(A, 5, 6);
と直接指定することも、
SetLength(A, B, C);
と、変数を使って指定することもできます。さらに多次元配列では、
SetLength(A[0], 5);
SetLength(A[1], 10);
と行ごとに要素数を変えることもできるのです。
これで任意の画像の大きさに対するデータ配列を作れるようになりました。
SetLength(Data, Bitmap.Width, Bitmap.Height);
とすればOKです。
さて、以上の方法で画像の大きさと同じサイズのメモリ領域が消費されるものの、座標=配列の添字、としてデータを操作できるようになりました。
ここからは、画像の回転は少々プログラムが複雑になるので、少し簡単な配列を使ってビットマップ画像にフィルターを掛けるプログラムを例に取っていきたいと思います。2次元配列を使った方が楽であるという点では同じです。
まずType宣言として、
//*********** RGB配列 ********** RgbStr = record B, G, R: Byte; //***** バイトの並びは B->G->R end; TRgbArray = array [0..65535] of RgbStr; PRgbArray = ^TRgbArray; //*********** 2次元RGB配列 ********** TRgbArray2 = array of array of RgbStr; PRgbArray2 = ^TRgbArray2; |
TRgbArray2というのが新しいTypeです。
TRgbArray2 = array of array of RgbStr;
としてRgbStr型の2次元動的配列を作りました。
この配列にデータを取り込む部分は、
//*********** ビットマップデータ取り込み *********** procedure TForm1.GetBit(var Bits:TRgbArray2); var y: Integer; begin //***** 配列の大きさを確定 Setlength(Bits, Image1.Height, Image1.Width); for y := 0 to Image1.Height -1 do begin CopyMemory(@Bits[y, 0], Image1.Picture.Bitmap.ScanLine[y], Image1.Width * Sizeof(RgbStr)); end; end; |
です。配列の大きさは
Setlength(Bits, Image1.Height, Image1.Width);
として指定しています。
実際にデータを読み込むにはScanLineで1行ずつ読んで、メモリをコピーすることになります。
CopyMemory(@Bits[y, 0], Image1.Picture.Bitmap.ScanLine[y],
Image1.Width * Sizeof(RgbStr));
CopyMemoryを使えば1行ずつではなくて、一気に画像データを読み込めそうに思えますが、実際は不可能です。
先に説明したようにDelphiの多次元動的配列では各行ごとに違うサイズが割り当てられるのですが、実は各行の要素の-1番目には配列の大きさが入っているからです。
[ , -1] | [ , 0] | [ , 1] | [ , 2] | [ , n] | |
[0 , ] | 3 | A | B | C | |
[1 , ] | 2 | D | E | ||
[2 , ] | 1 | F |
ここにビットマップデータをブロックで読み込むと正しく動作しないですね。
さて、逆に操作したデータをビットマップに反映するのは上と逆のことを行えばいいだけなので省略します。
さて、読み込んだデータに対してエフェクトを掛けるわけですが、ここではフォトショップ等で使われているフィルターという効果を使用します。フィルターとは、あるピクセルに対して上下左右と斜めに隣接するピクセルとの関連で、新たにそのピクセルの色を設定するというものです。
極端な例ですが、ビットマップのある部分を拡大したところ 左のようだったとします。 | |||
0 | 1 | 0 | フィルター処理では左のようなマトリックスを用いて、各マス のRGB値に対してかけ算を行って、合計した値を真ん中のマ スに入れます。 |
1 | -4 | 1 | |
0 | 1 | 0 |
この処理をビットマップ上の全ピクセルに施した結果として、画像の輪郭を強調したような効果が生まれます。 (例題のプログラムは処理を簡略化した「フィルターもどき」です。フィルターに関しては、AK's Laboratoryで詳しく解説されていますので、そちらを参考にして下さい) |
では、処理の内容を見てみましょう
(引用が長くなるので、一部省略しています。詳細はサンプルのソースを見て下さい)
//*********** エフェクトルーチン *********** procedure TForm1.btnArray2Click(Sender: TObject); var x, y: Integer; Src, Dst: TRgbArray2; //***** 動的配列 (・・・一部省略・・・) |
ここでは、処理前と処理後のデータ用として、SrcとDstという二つのTRgbArray2型配列を使っています。
フィルター用配列は、
Fl: array [-1..1, -1..1] of Double;
という2次元配列なので、2次元配列同士でぶつけあうことができます。
Rv := Rv + Src[y - i,x + j].R * Fl[i, j];
DelphiのStringGridでは[行, 列]という扱いになっているので、それに合わせて[y,
x]としていますが、逆にxyの並びにしてもいいと思います。その場合は、SetLengthの所とフィルター配列を読む部分を変える必要があります。
以上で、画像を扱うプログラムの高速化と効率化の説明は終わりです。