コラム / もーのーくーろー / ピクセル単位のループを書く


コラム/もーのーくーろー

モノクロボタンを押したときの処理を書きます。
モノクロ化する部分はおいといて、
画像の全体を、がーっと スキャンする部分だけ。
モノクロボタンのonclickを編集します。
procedure TForm1.Button3Click(Sender: TObject);
var
   PBit:PByteArray;
   x,y:Integer;
   R,G,B:Byte;
   Bmp:TBitmap;
begin
   Bmp:=TBitmap.Create;
   Bmp.Assign(Image1.Picture.Graphic);

   Bmp.PixelFormat:=pf24bit;
   For y:=0 to Bmp.Height-1 do begin
       For x:=0 to Bmp.Width-1 do begin
           PBit:=Bmp.ScanLine[y];
           B:=PBit[x*3  ];
           G:=PBit[x*3+1];
           R:=PBit[x*3+2];
       end;
   end;

   Image1.Picture.Graphic.Assign(Bmp);
   Bmp.Free;
end;
うへえ。このコード書き出すのに、
またもやスレ住民の助けをコウタこうたKO歌おっぴょー
jpegファイルを開いているときは、
(というか、bmp以外を開いているときは
Bitmapプロパティにアクセスするとデータが飛ぶ
仕様らしいです。
なので、いったん全部作業領域に転送しないといけません。
この処理をしないでいったんコードを書いたところ、
Image1の画像がなくなってしまいましたwwwww
Delphiの定石?で、
Image1等にいったん読み込んだ画像を、
ピクセル単位で編集したりしたいときは、
TBitmapクラスを作って、インスタンスにクローンを転送してから、
操作して、その後でまた、
Image1にTBitmapクラスのインスタンスのクローンを転送して、
反映させる
というのがパターンみたいです。

kuron.png

Bmp:TBitmapを宣言して、
   Bmp:=TBitmap.Create;
生成
   Bmp.Assign(Image1.Picture.Graphic);
Image1のデータを貰う。
この2行で、
Bmpのインスタンスが、
Image1の表示されてる画像のクローンになります。
で、最終的に、
   Image1.Picture.Graphic.Assign(Bmp);
   Bmp.Free;
で、Image1にBmpのクローンを転送して、
Bmpを解放してます。
クローンってのは、全く同じ って意味です。
別にDel用語でもなんでもないです。
ただ、Assign関数ではなくて、
Bmp:=Image1.Picture.Graphic;
と、ただの代入でコピーしようとすると、
「インスタンスを共有するだけ」
で、どっちの変数からも、同じものを指しちゃうので、
クローンじゃなくて、一身同体、
肉体と精神、マテリアルとメンタルとアストラル
コピー先から触っても、
ああん いやん びゅうーー
と、消えちゃうわけ。
で、クローンを作ったところで、
そのクローンに対して、
ピクセル単位のループをするわけです。
   Bmp.PixelFormat:=pf24bit;
   For y:=0 to Bmp.Height-1 do begin
       For x:=0 to Bmp.Width-1 do begin
           PBit:=Bmp.ScanLine[y];
           B:=PBit[x*3  ];
           G:=PBit[x*3+1];
           R:=PBit[x*3+2];
       end;
   end;
まず、
   Bmp.PixelFormat:=pf24bit;
で、
ビットマップデータを、
RGBそれぞれ0-255の形式に変えます。
他には、ARGBで透明度を持った32Bitとか、
プロのドット絵師じゃないと絵にみえねーえよ
みたいな、超パレットが少ないのとか
いろいろあるっぽいんですが、
24Bitにしとけばいいとおもいます。
24Bitってのは、容量にして3Byteで、
1ピクセルあたり3Byteあります。
ピクセルの開始位置から、
1バイト目が青
2バイト目が緑
3バイト目に赤
が入ってます。
んで、2重ループします。
   For y:=0 to Bmp.Height-1 do begin
        For x:=0 to Bmp.Width-1 do begin
         //略
        end;
   end;
yとxはカウンタ変数で、それぞれ、
Height-1,Width-1が最大値ですから、
yは0から縦の最大値、xは0から横の最大値
ピクセルをカウントして、
結果的に、全ピクセルを参照します。
で、ループの中身です。
ここが問題。
           PBit:=Bmp.ScanLine[y];
           B:=PBit[x*3  ];
           G:=PBit[x*3+1];
           R:=PBit[x*3+2];
一行目がややこしいです。
           PBit:=Bmp.ScanLine[y]; 
で、PBitに、Bmp.ScanLine[y]の返り値を代入しています。
PBitの型は、
    PBit:PByteArray;
と宣言されてます。
PByteArrayというのは、
「Byte型の配列のポインタ」
です。
ポインタというのは、
「メモリ上のデータのアドレスを指し示すもの」
です。
メモリってのは、パソコンの中の、データを入れておく場所のひとつです。
プログラムが実行されている時、
そのプログラムで使うデータは、メモリの中にあります。
逆に言うと、
「実行中にデータを入れておくところがメモリ」
と言った感じです。
で、アドレスってのは、その場所の事です。
データは、メモリのどこかにあるわけですから、
場所さえわかればアクセスできます。
つまり、
「Byte型の配列のポインタ」
というのは、
「Byte型の配列」っていうデータがメモリ上のどっかにあって、
それのアドレスを保存するための変数ですよ
ってことなわけです。
Byte型の配列ってのは、Byte型のデータが並んだものの事です。
配列ですから、 配列名[要素番号]
というように参照できます。
それを踏まえて、

           PBit:=Bmp.ScanLine[y]; 
を見てください。
PBitは、「Byte型の配列のポインタ」ですから、
「Byte型の配列のアドレスが入るモノ」です。
当然、右辺の、Bmp.ScanLine[y]
というのは、
「Byte型の配列のアドレス」
を返す関数って事になりますね。
ScanLineは、配列っぽい見た目の(?)プロパティで、
[ ] でy座標を与えると、そのピクセル行の開始アドレスを返します。
今回は、[y]ってのは、Forループのカウンタ変数ですから、
今ループしてる行のアドレスを返すわけですね。

sl.png

さて、PBitに、開始アドレスが入る仕組みはわかったと思います。
残りは、

           B:=PBit[x*3  ];
           G:=PBit[x*3+1];
           R:=PBit[x*3+2];
です。
B,G,RはByte型です。(宣言を参照
Byte型ですから、
PByteArray、バイト型の配列の、配列の一つの要素の型です。
つまり、
PBitに要素番号を与えて、一つのバイト型を呼び出せば、
R,G,Bに代入できるわけです。
ここで、 
   Bmp.PixelFormat:=pf24bit;
を思い出してください。
24Bit形式にしましたから、
青の値、緑の値、赤の値 が、1バイトずつ並んでます。
ってことは、
PBitの要素一個には、
青か、緑か、赤のどれかが入ってるわけです。
んじゃぁ、
三個ずつ見ていけば、
青、緑、赤、   青、緑、赤、   青、緑、赤
って感じで、
1ピクセルごとの色情報が貰えちゃうじゃないか
ってことになるわけです。
ScanLineは行の開始アドレスを返したのですから、
一番最初、すなわち、PBit[0]
は、
1ピクセル目の青が入ってるわけです。
で、それ以降、
0,3,6,9,12...番目には青が入ってくるわけです。
これ、ループカウンタのxを使ってうまく表せると
思いません?
x*3 なんですYOOOOOOOOOOOOOOふぉーーーーーーーーーーーーー
。。。だから中学校レベルの数学をそれっぽく語るな!
てことは、緑は、
1,4,7,10,13...
赤は、
2,5,7,11,14...
に入ってるわけですが、
これの式も、
x*3+1
x*3+2
となるわけです!!!!!!!!!!!!!!
わおおおwwwwwwwwwwwwwwwwwwwwwwww
これで、
           PBit:=Bmp.ScanLine[y];
           B:=PBit[x*3  ];
           G:=PBit[x*3+1];
           R:=PBit[x*3+2];
は、行の開始アドレスを貰う
R,G,Bの値を貰う
の2プロセスなわけです。
まとめると
   Bmp:=TBitmap.Create;
   Bmp.Assign(Image1.Picture.Graphic);

   Bmp.PixelFormat:=pf24bit;
   For y:=0 to Bmp.Height-1 do begin
       For x:=0 to Bmp.Width-1 do begin
           PBit:=Bmp.ScanLine[y];
           B:=PBit[x*3  ];
           G:=PBit[x*3+1];
           R:=PBit[x*3+2];
       end;
   end;

   Image1.Picture.Graphic.Assign(Bmp);
   Bmp.Free;
Bmpを生成する
BmpにImageのクローンを転送する
Bmpを24Bit形式にする
行の開始アドレスを貰う
そこから、青、緑、赤を抜き取る
ImageにBmpのクローンを転送する
Bmpを解放する
てなわけです。
長くなった。