小学校教員のためのプログラミング入門
今回は画像を読み込んで,いろいろ扱ってみるスケッチを作っていきます.
まずは単純に画像を表示するスケッチを作ってみます.
Processing で画像を表示するのは非常に簡単です.まず,新しいスケッチを作る準備をした後,適当な名前を使って保存をしてください.これは,表示する画像を入れておく場所を作るためです.
次に表示する画像ファイルを作成しているスケッチの pde ファイルと同じ場所(スケッチが保存されるフォルダ内にできた現在のスケッチ名のフォルダの中)に保存してください.画像ファイルはなんでも構いません.なにもないという人は下の画像をダウンロードして利用してください.
この画像を表示するスケッチコードは次のようになります.
PImage photo; void setup() { size( 800,600 ); photo = loadImage( "20081207-3.jpg" ); } void draw() { image( photo, 0, 0 ); } |
スケッチで画像を扱うときには,画像を保存・操作するための PImage クラスのインスタンス(通称として PImage オブジェクトと呼びます)を生成します.クラスやインスタンスはプログラミング言語の種類を解説したときに出てきたオブジェクト指向プログラミングの用語です.
クラスとはオブジェクトの性質を定義するためのプロパティとオブジェクトを操作するためのメソッドの集合で定義されます.これらのプロパティやメソッドのことをメンバメソッド,メンバプロパティと呼びます.
まず PImage オブジェクトを格納するための変数を宣言します.PImage オブジェクトはコード全体から利用することになるので,この変数はグローバル変数として宣言しておきます.
PImage photo;
画像ファイルを読み込んで扱いたいときには,loadImage メソッドを次のように利用します.
PImage型の変数 = loadImage( ファイルパス名 );
loadImage メソッドはファイルパス名を引数として受け取り,PImage オブジェクトを生成します.ファイルパス名は,実行フォルダの位置,PDE 環境で実行させているときは pde ファイルがあるフォルダからの相対パスで指定します.たとえば,data というフォルダを作りその中に画像を入れた場合は data/ファイル名 と書きます.今回は pde ファイルと同じ場所に置いてありますので,単にファイル名を書くだけで構いません.
なおファイルパス名はダブルクォーテーション"で囲みます.ダブルクォーテーションで囲んだものを 文字列リテラル と呼び,Processing における値の一種類です.
PImage オブジェクトを表示するには,image メソッドを利用します.image メソッドは
image( PImageオブジェクト, 表示する画像左上の X 座標, 表示する画像左上の Y 座標 );
と使います.
現在,画像は描画ウィンドウの左上に表示されています.これを中央に表示するためには,画像と描画ウィンドウの隙間を,画像と描画ウィンドウの幅の差を二等分した量にすればよいので,image メソッドを用いて描画を行っている文を次のように変更します.
image( photo, (width-photo.width)/2, (height-photo.height)/2 );
PImage オブジェクトの横幅と縦幅は,PImage オブジェクトが格納されている変数の名前を使って 変数名.width,変数名.height で取得できます.表示ウィンドウの横幅と縦幅は width と height で取得でき,それらから画像を中央に表示させるときの左上座標は上記のような式で指定することができます.
次に画像を動かしてみましょう.以前に作ったボールがバウンドするスケッチで,バウンドするものを図形ではなく画像にしてみます.
次に以前に作ったスケッチのコードを再度掲示しておきます.
//------------------------------------------------------------------------- // 基本的な物理法則に従った,投げ出したボールの軌跡表示 //------------------------------------------------------------------------- int r = 10; float x0 = r, y0 = r; float g = 9.8; float v0 = 30; float theta = PI/8; float t = 0; float x = x0; float y = y0; void setup() { size(300,300); colorMode(RGB,255,255,255,100); ellipseMode(RADIUS); background(0,0,0); frameRate(50); } void draw() { stroke( 0, 0, 0, 10 ); fill( 0, 0, 0, 10 ); rect( 0, 0, width, height ); x = x0 + v0*cos(theta)*t; y = y0 + v0*sin(theta)*t+g*t*t/2.0; stroke( 0, 0, 100, 100 ); fill( 0, 200, 255, 100 ); ellipse( x, y, 10, 10 ); t = t + 0.1; if( y > height-r ) { float vx0 = v0*cos(theta); float vy0 = v0*sin(theta)+g*t; vy0 = -vy0 * 0.9; v0 = sqrt( vx0*vx0 + vy0*vy0 ); theta = atan( vy0/vx0 ); if( vx0 < 0 ) theta = theta + PI; x0 = x; y0 = height-r; t = 0; } if( x > width-r ) { float vx0 = v0*cos(theta); float vy0 = v0*sin(theta)+g*t; v0 = sqrt( vx0*vx0 + vy0*vy0 ); vx0 = -vx0; theta = atan( vy0/vx0 ); if( vx0 < 0 ) theta = theta + PI; x0 = width-r; y0 = y; t = 0; } if( x < r ) { float vx0 = v0*cos(theta); float vy0 = v0*sin(theta)+g*t; v0 = sqrt( vx0*vx0 + vy0*vy0 ); vx0 = -vx0; theta = atan( vy0/vx0 ); if( vx0 < 0 ) theta = theta + PI; x0 = 0; y0 = y; t = 0; } } |
まず,動かす画像の準備をします.ボールの代わりになるような画像がよいので,ペイントソフトなどで描いてみてください.ポイントは背景を透明にした GIF 形式の画像にすることです.面倒な人は下の画像を利用してください.画像は先と同じように pde ファイルと同じ場所に保存します.新しくスケッチを作成した場合は,まず保存を行ってフォルダを作成してください.
先と同じように,PImage オブジェクトを格納するための変数の宣言と,setup メソッドの中で画像の読み込みを行います.
PImage sprite; : void setup() { : sprite = loadImage("dora.jpg"); : }
今まで ellipse メソッドで円を描いていた部分を image メソッドで画像を描くように変更します.
image( sprite, x, y );
ellipse メソッドは円の中心座標を指定していましたが,image メソッドは画像の左上座標を指定しています.そのため,跳ね返りの判定部分を変更します.
一番簡単なのは左壁にぶつかったときです.画像の左端が描画ウィンドウの左端より左になったときなので,その条件は x < 0 となります.
同様に,右壁にぶつかったときは,画像の右端が描画ウィンドウの右端より右になったときで,画像の横幅は sprite.width なので,条件は width < x+sprite.width となります.最後に,地面にバウンドするのは画像の下端が描画ウィンドウの下端より下になったときで,画像の縦幅は sprite.height なので,条件は y+sprite.height > height となります.
そこで,コードを次のように書き換えます.
if( y+sprite.height > height ) { : y0 = height-sprite.height; : } if( x+sprite.width > width ) { : x0 = width-sprite.width; : } if( x < 0 ) { : x0 = 0; : }
最後に,半径 r の概念がなくなったので,その宣言の削除をし,代わりに画像の横幅と縦幅を入れておく w と h を宣言します.w と h の初期化は画像を読み込むまでできないので setup メソッドの中に入れます.x0 と y0 の初期値は適当に入れておきます.
できあがったスケッチコードを次に示します.なお,このコードでは背景を白色にしています.
//------------------------------------------------------------------------- // ドラえもんがバウンドする //------------------------------------------------------------------------- PImage sprite; int w,h; float x0 = 0, y0 = 0; float g = 9.8; float v0 = 30; float theta = PI/8; float t = 0; float x = x0; float y = y0; void setup() { size(300,300); colorMode(RGB,255,255,255,100); ellipseMode(RADIUS); background(255,255,255); frameRate(50); sprite = loadImage("dora.gif"); w = sprite.width; h = sprite.height; } void draw() { stroke( 255, 255, 255, 10 ); fill( 255, 255, 255, 10 ); rect( 0, 0, width, height ); x = x0 + v0*cos(theta)*t; y = y0 + v0*sin(theta)*t+g*t*t/2.0; stroke( 0, 0, 100, 100 ); fill( 0, 200, 255, 100 ); image( sprite, x, y ); t = t + 0.1; if( y+h > height ) { float vx0 = v0*cos(theta); float vy0 = v0*sin(theta)+g*t; vy0 = -vy0 * 0.9; v0 = sqrt( vx0*vx0 + vy0*vy0 ); theta = atan( vy0/vx0 ); if( vx0 < 0 ) theta = theta + PI; x0 = x; y0 = height-h; t = 0; } if( x+w > width ) { float vx0 = v0*cos(theta); float vy0 = v0*sin(theta)+g*t; v0 = sqrt( vx0*vx0 + vy0*vy0 ); vy0 = -vy0; theta = PI+atan( vy0/vx0 ); x0 = width-w; y0 = y; t = 0; } if( x < 0 ) { float vx0 = v0*cos(theta); float vy0 = v0*sin(theta)+g*t; v0 = sqrt( vx0*vx0 + vy0*vy0 ); vy0 = -vy0; theta = atan( vy0/vx0 ); x0 = 0; y0 = y; t = 0; } } |
PImage オブジェクトは,画像内の 1 画素ごとにどんな色なのかを調べることができます.また,1 画素ごとに変更することも可能です.たとえば,PImage 型の変数 photo に画像が読み込んであるとき,その画像の左上を原点とし,右方向を X 軸性方向,下方向を Y 軸正方向とする座標系において座標が (x,y) の点の色は,PImage クラスのメンバメソッド get か,メンバプロパティ pixels を用いて,
photo.get( x, y ) または photo.pixels[ y*image.width + x ]
として取得することができます.得られる値の型は color 型です.image.pixels は color 型の配列で,(0,0), (1,0), (2,0),...(image.width-1,0), (0,1), (1,1), (2,1),...,(image.width-1,image.height-1) の画素の色が順番に格納されています.
画素の色を変更するときは,PImage クラスのメンバメソッド set,メンバプロパティ pixels を用いて,
photo.set( x, y, color( 255, 0, 0 ) ); または photo.pixels[ y*image.width + x ] = color( 255,0,0 );
とすると,座標が (x,y) の点の色が赤色になります.なお,get や set よりも pixels を利用した方が,高速に動作するスケッチになります.また,これらの方法で画素の色を取得する前には PImage クラスのメンバメソッドである loadPixels を呼び出し,また変更後には updatePixels を呼び出します.
最初のスケッチを若干変更した下のスケッチは画像の上半分が赤色に塗りつぶされます.
PImage photo; void setup() { size( 800,600 ); photo = loadImage( "20081207-3.jpg" ); photo.loadPixels(); for( int y = 0 ; y < photo.height/2 ; y++ ) { for( int x = 0 ; x < photo.width ; x++ ) { photo.set( x, y, color(255,0,0) ); } } photo.updatePixels(); } void draw() { image( photo, (width-photo.width)/2, (height-photo.height)/2 ); } |
また,
color cl = photo.get( x, y ); photo.set( x, y, color( 128, green(cl), blue(cl) ) ); または color cl = photo.pixels[y*photo.width+x]; photo.pixels[y*photo.width+x] = color( 128, green(cl), blue(cl) );
とすると,青と緑の成分はそのままに,赤の成分だけ50%にすることができます.green メソッドは
green( color型の値 )
と書くことで color 型の値の緑成分の値を得ることができるものです.blue メソッドも同様で,red メソッドもあります.また,hue,saturation,brightness メソッドはそれぞれ,色相,彩度,明度を得るためのメソッドです.
読み込んだ画像をグレースケールで表示するスケッチを作ってみます.基本は上記のスケッチで,画素の色に従って,それをグレースケールの値に変更するだけです.
RGB で表されたグレースケールに変更する方法にはいろいろあります.もっとも単純な単純平均法は
Y=(R+G+B) / 3
とします.ここで R,G,B は赤,緑,青それぞれの成分の値で,Y は変換後の R,G,B 共通の値となるものです.また,中間値法は
min = {R,G,Bのうちの最小値} max = {R,G,Bのうちの最大値} Y = (min + max) / 2
Gチャンネル法は,
Y = G
とします.G チャンネル法は心理的には緑がもっとも強く感じているため,白黒にするときは緑だけ着目すればよいという強引な手法です.今回は,比較的きれいなグレースケールとなる NTSC 係数による加重平均法を用います.これは,
Y = 0.298912*R + 0.586611*G + 0.114478*B
とします.この変換式を先のスケッチにあてはめると
color cl = photo.get(x,y); float Y = 0.298912*red(cl) + 0.586611*green(cl) + 0.114478*blue(cl); photo.set( x, y, color( Y, Y, Y ); または color cl = photo.pixels[y*photo.width+x]; float Y = 0.298912*red(cl) + 0.586611*green(cl) + 0.114478*blue(cl); photo.pixels[y*photo.width+x] = color( Y,Y,Y );
となります.なお,for 文の y の値の範囲を photo.height/2 から photo.height に変更して,画像全体に変更を加えるようにしてください.
上記までのスケッチは読み込んだ画像自体を変換してしまいましたが,今回は一部分だけ,それも,一時的に書き換えるということをして,マウスカーソルの周辺だけグレースケールにするというスケッチにしてみます.
上記のことを実現するには,PImage の内容(画像)を書き換えるのではなく,描画ウィンドウに表示されている内容を書き換えるという方法を用います.
そのためには組み込みメソッドの loadPixels を利用します.loadPixels メソッドを実行すると,そのときに描画ウィンドウに表示されている画像がシステム変数 pixels に格納されます.この内容を書き換えた後,updatePixels メソッドを呼び出すと,変更が描画ウィンドウ内に反映されます.
なお,PImage クラスのメンバメソッドである get と set が メンバ変数の pixels を操作できたのと同じように,上記の pixels を操作するための getメソッド,set メソッドも用意されています.
この方法を用いて上半分をグレーにするスケッチは次のようになります.なお,pixels には描画ウィンドウ全体が格納されますので,for 文の中で画像の幅(photo.width, photo.height)を使っている部分は,描画ウィンドウの幅(width, height)に変更しています.
PImage photo; void setup() { size( 800,600 ); photo = loadImage( "20081207-3.jpg" ); } void draw() { image( photo, (width-photo.width)/2, (height-photo.height)/2 ); loadPixels(); for( int y = 0 ; y < height/2 ; y++ ) { for( int x = 0 ; x < width ; x++ ) { color cl = pixels[y*width+x]; float Y = 0.298912*red(cl) + 0.586611*green(cl) + 0.114478*blue(cl); set( x, y, color( Y, Y, Y ) ); } } updatePixels(); } |
このスケッチは,draw メソッドが呼び出されるたびに PImage オブジェクトである photo に読み込まれた画像を表示しなおし,その一部分だけ,描画ウィンドウに表示されている内容を直接変更するという動きになっています.
目的のスケッチは,draw メソッドが呼び出されるたびに,マウスの周辺だけを書き換えればよいので,まずは,マウスを中心とした四角領域の左上座標と右上座標を計算します.
マウスの座標は mouseX と mouseY で取得できます.四角領域の1辺の長さを len に入れておくとすると,目的の座標は (mouseX-len/2, mouseY-len/2), (mouseX+len/2, mouseY-len/2) となります.
ただ,この座標が描画ウィンドウの外側になると,配列 pixels を参照するときにエラーが発生してしまいます.そこで,X 座標は 0 から width の範囲,Y 座標は 0 から height の範囲になるように書きかえる必要があります.すると,たとえば,左上の X 座標は
x = mouseX-len/2; if( x < 0 ) x = 0; if( x >= width ) x = width-1;
と修正する必要があります.これは面倒ですね.このように範囲を制限して値を用いたいときに便利なのが constrain メソッドです.constrain メソッドは
constratin( 値,最小値,最大値 )
とかくと,最小値と最大値の範囲内であれば値そのものを,範囲外であれば最小値もしくは最大値を生成します.これを用いると,先の左上の X 座標を求める 3 行は
x = constratin( mouseX-len2, 0, width-1 );
と書きかえることができます.
上記の方法を用いると,マウスを中心とした四角領域だけを書き換えるには,for 文のところを
int x1 = constrain( mouseX-len/2, 0, width-1 ); int y1 = constrain( mouseY-len/2, 0, height-1 ); int x2 = constrain( mouseX+len/2, 0, width-1 ); int y2 = constrain( mouseY+len/2, 0, height-1 ); for( int y = y1 ; y <= y2 ; y++ ) { for( int x = x1 ; x <= x2 ; x++ ) {
と書き換えればよいはずです.
読み込んだ画像をグレースケールで表示するスケッチをもとにして,読み込んだ画像をセピア調で表示するスケッチを作ってみましょう.セピア調には,RGB の各成分を,グレースケールにしたときの Y の値を用いて次の式で求まる値にすることで変換できます.
R = Y * 240 / 256; G = Y * 200 / 256; B = Y * 148 / 256;