Processing は簡単にデータを視覚化できるようにしているだけでなく、マウスおよびキーボードからのユーザー入力もサポートします。Processing がユーザー入力のサポートを可能にしている手段は、ユーザーが入力したことを Processing プログラムに通知する、一連の関数とコールバックです。
Processing には、キーが押されたこと、またはリリースされたことを Processing アプリケーションに通知するためのキーボード関数がいくつか用意されています。さらに、ユーザーが特殊文字を使用できる場合には、その入力を詳しく構文解析することも可能です。
キー押下イベントがあったことを伝えるには、keyPressed
関数を使用します。この関数をアプリケーションに定義すると、キー押下イベントが発生するたびに、この関数が呼び出されます。ユーザーが押した実際のキーを識別するには、このコールバック関数の中で key
という特殊な変数を使用します。同じように、キーがリリースされた場合のイベントも keyReleased
関数を使用することで捕捉することができます。どちらの関数も同じ情報を提供しますが、アクションをトリガーするタイミングは自由に定義できるようになっています。
リスト 1 に、keyPressed
関数と keyReleased
関数を記載します。いずれの関数の中でも、プログラムがユーザーによる 2 つのタイプのキー・ストロークを解析します。2 つのタイプとは、ASCII 文字と非 ASCII 文字 (矢印キーなど) のことで、前者はコード化されませんが、後者の場合はコード化が必要です。コード化された文字に対しては、Processing は key
変数を CODED
トークンに設定することによって、keyCode
というもう 1 つの特殊な変数を調べるように指示します。したがって、key
が CODED
でなければ、key
変数にキー・ストロークが含まれるということです。一方、key が CODED
に設定されている場合は、keyCode
変数に実際の文字 (UP
、DOWN
、LEFT
、RIGHT
、ALT
、CONTROL
、または SHIFT
) が含まれます。
リスト 1.
keyPressed
および keyReleased
コールバックvoid keyPressed() { if (key == CODED) { if (keyCode == DOWN) println("Key pressed: Down arrow"); if (keyCode == SHIFT) println("Key pressed: Shift key"); } else { println("Key pressed: " + key ); } } void keyReleased() { if (key == CODED) { if (keyCode == DOWN) println("Key released: Down arrow"); if (keyCode == SHIFT) println("Key released: Shift key"); } else { println("Key released: " + key ); } } |
Processing 関数の中で使用できる特殊な変数には、keyPressed
という変数もあります。keyPressed
関数はブール値を返して、押されているキーがあること (true)、またはどのキーも押されていないこと (false) を示します。この変数を使用することで、Processing アプリケーション内 (通常のコールバック構造の外部) で発生したキー・イベントを管理することができます。
マウス・イベントはキーボード・イベントと同様の構造に従いますが、キーボード・イベントとは異なり、発生する可能性のあるさまざまなマウス・ベースのイベントをサポートするために、複数の関数があります。マウス・イベントに定義できる基本的なコールバックは、以下の 4 つです。
mousePressed
mouseReleased
mouseMoved
mouseDragged
mousePressed
コールバック関数は、ユーザーがマウス・ボタンを押下すると呼び出されます。どのマウス・ボタンが押下されたのかは、このコールバック関数に含まれる mouseButton
変数 (LEFT
、CENTER
、RIGHT
) によって特定することができます。そしてマウス・ボタンがリリースされると、mouseReleased
コールバック関数が呼び出されます。mousePressed
と mouseReleased
を組み合わせたコールバック関数 mouseClicked
を使用することもできます。mouseMoved
関数は、マウス・ボタンが押下されていない状態でマウスが動かされると呼び出されます。最後の mouseDragged
関数は、マウス・ボタンが押下された状態でマウスが動かされると呼び出されます。
マウス・イベントにも、数々の特殊な変数を使用することができます。まず、mouseX
および mouseY
変数にはマウスの現在の位置が取り込まれます。マウスが移動する前の位置を (前のフレームから) 取り込むには、pmouseX
変数と pmouseY
変数を使用します。また、マウス・ボタンが現在クリックされているかどうかを検出するには、Processing アプリケーション内で mouseClicked
変数を使用することができます。この変数を mouseButton
と組み合わせれば、現在クリックされているボタンを特定することもできます。リスト 2 に、マウス・イベントのコールバックと特殊変数を記載します。
リスト 2. 基本的なマウス・イベント・コールバックのデモ
int curx, cury; void setup() { size(100, 100); curx = cury = 0; } void mousePressed() { println( "Mouse Pressed at " + mouseX + " " + mouseY ); if (mousePressed && (mouseButton == LEFT)) { curx = mouseX; cury = mouseY; } } void mouseReleased() { println( "Mouse Released at " + mouseX + " " + mouseY ); } void mouseMoved() { println( "Mouse moved, now at " + mouseX + " " + mouseY ); } void mouseDragged() { println( "Mouse moved from " + curx + " " + cury + " to " + mouseX + " " + mouseY ); } |
以上で説明したマウス・イベントとキーボード・イベントの関数が、UI を作成する際の基礎となります。オブジェクトをディスプレイ・ウィンドウに配置した後、マウス・イベント関数を使用して、マウス・ボタンが押されたかどうかを判断することができます。つまり、マウス・ボタンが押されたときに、オブジェクトの領域内にあるピクセルがマウスのカーソルであったかどうかを識別できるということです。
Processing では、オブジェクト指向プログラミング (OOP: Object-Oriented Programming) の手法を利用して、アプリケーションの開発を単純化し、さらにアプリケーションを保守しやすくすることができます。Processing 自体はオブジェクト指向ですが、オブジェクトの概念を無視したアプリケーションを開発することも可能です。けれども、OOP には情報の非表示、モジュール性、そしてカプセル化という利点があるため、OOP を使用することには重要な意味があります。
Processing は他のオブジェクト指向の言語と同じく、Class
の概念を使用してオブジェクト・テンプレートを定義します。クラスから定義されたオブジェクトが保持するのは、データとそのデータに対して実行できる操作の一式です。これを説明するため、まずは単純なクラスを開発するところから始めます。その後、この例を展開させて、開発したクラスに複数のオブジェクトを組み込みます。
Processing でのクラスは、データとそのデータに適用される関数 (またはメソッド) の一式を定義します。この例で引用するデータは、2 次元空間の座標 (x、y) と直径によって、中心と大きさが指定される円のデータです。この円は、x 座標と y 座標、そして直径として 1 を指定して初期化することができますが、この場合を単に「円が指定されている」と言うことにします。円を初期化するための情報は、init
関数を使用して指定します。新たに描画される円は、前に描画された円よりも大きい円になります。直径がゼロより大きければ (円が指定されているオブジェクトであることを意味します)、その直径を増分して円のサイズを大きくします。直径を増分するのは、spread
関数です。そして最後に、show
関数が円をディスプレイ・ウィンドウに描画します。直径がゼロより大きい限り (つまり、有効な円である限り)、ellipse
関数を使用して円を作成し続け、円が特定の大きさになった時点で直径をゼロに設定し、円を取り消します。このサンプル・クラスは、リスト 3 のとおりです。
リスト 3. 新たに描画される円が前に描画された円よりも大きくなる、円 (ドロップ) のサンプル・クラス
class Drop { int x, y; // Coordinate (center of circle) int diameter; // Diameter of circle (unused == 0). void init( int ix, int iy ) { x = ix; y = iy; diameter = 1; } void spread() { if (diameter > 0) diameter += 1; } void show() { if (diameter > 0) { ellipse( x, y, diameter, diameter ); if (diameter > 500) diameter = 0; } } } |
今度は、この Drop
クラスを使用して、ユーザー入力を用いたグラフィックを作成する方法を見てみましょう。リスト 4 に、Drop
クラスを使用したアプリケーションを記載します。最初のステップは、ドロップの配列 (drops
という名前の配列) を作成することです。配列を作成した後には、いくつかの定義が続きます (ドロップの数と、現在対象としているドロップの (drops
配列における) インデックス)。setup
関数では、ディスプレイ・ウィンドウを作成して drops
配列を初期化します (すべての直径がゼロに設定されます。つまり円が指定されていません)。ドロップのコア機能はこのクラス自体の中に含まれるため (リスト 3 の spread
関数と show
関数を参照)、draw
関数はかなり単純な内容になっています。最後に UI 部分を追加して、ユーザーがドロップの開始位置を定義できるようにします。具体的には、mousePressed
コールバック関数がマウスの現在の位置の情報を使ってドロップを初期化した後 (これで、ドロップには直径が設定され、使用されることになります)、現在対象としているドロップを表すインデックスをインクリメントするという内容です。
リスト 4. 複数のユーザー定義ドロップを作成するアプリケーション
Drop[] drops; int numDrops = 30; int curDrop = 0; void setup() { size(400, 400); ellipseMode(CENTER); smooth(); drops = new Drop[numDrops]; for (int i = 0 ; i < numDrops ; i++) { drops[i] = new Drop(); drops[i].diameter = 0; } } void draw() { background(0); for (int i = 0 ; i < numDrops ; i++) { drops[i].spread(); drops[i].show(); } } void mousePressed() { drops[curDrop].init( mouseX, mouseY ); if (++curDrop == numDrops) curDrop = 0; } |
図 1 に、リスト 3 とリスト 4 によるアプリケーションの出力を示します。ご覧のように、マウスが何度もクリックされたことによって、大きさの異なるドロップがいくつも描画されています。
図 1. リスト 3 および 4 によるアプリケーションのディスプレイ・ウィンドウ
Processing には、画像処理に役立つ興味深い機能が用意されています。このセクションでは、画像のフィルター、合成、そしてピクセルを使ったユーザー定義の画像処理のサポートについて詳しく説明します。
Processing は、filter
関数を介して、あらかじめ用意されている画像処理機能を提供します。この filter 関数は、フィルターのモードを直接ディスプレイ・ウィンドウに適用する関数です。リスト 5 に、フィルターを使用した単純な Processing アプリケーションを示します。図 2 に示すさまざまなタイプの出力は、このアプリケーションに関連する出力で、リスト 5 では BLUR
のフィルターを適用しているだけですが、図 2 にはその他にも適用できるフィルターを指定したときの画像が (対応するコードと一緒に) 示されていることに注意してください。
filter
関数はディスプレイ・ウィンドウの表示内容に直接作用するため、この関数を使用するときに指定する必要があるのは、フィルターのモード (適用するフィルターのタイプ) と画質 (つまり、フィルターのモードに対する引数) だけです。リスト 5 は、画像を保管するためのデータ型である PImage
型の変数を宣言するところから始まっています。これに続き、setup
関数の中で特定の画像をPImage
データ型の変数 (img1
) にロードします。画像がロードされた時点で、画像のサイズが既知となるため、その画像のサイズを使ってウィンドウのサイズを設定します (PImage
インスタンスの width
および height
属性を使用)。続く draw
関数の中で、image
関数を呼び出して画像を表示します。image
関数は画像の左上隅の x 座標と y 座標を指定して、画像をディスプレイ・ウィンドウに表示するように要求します (画像の幅と高さを指定することも可能です)。そして最後に、フィルターを指定して適用するという流れです。この例では、BLUR
モードを指定していますが、図 2 には他のフィルターを指定したときの画像も示してあるので、オリジナルの画像と比べてみてください。
※訳注: 上記段落の原文の最後にある「(also provided in Figure 5 below)」は、誤りであり、訳出すると読者が混乱すると思われるので訳出していません。
リスト 5. 単純なフィルター・アプリケーション
PImage img1; void setup() { img1 = loadImage("alaska1.png"); size(img1.width, img1.height); smooth(); } void draw() { image(img1, 0, 0); filter(BLUR, 2); } |
図 2 に示されているように、Processing には、画像を操作するアプリケーションで一般的に使われている画像処理の操作があらかじめ用意されていますが、ピクセル単位で画像を操作することもできます。
図 2. フィルター操作の例
上記に示されていないフィルターの種類には、以下のものがあります。
OPAQUE
— アルファ・チャネルを不透明に設定します。ERODE
— 指定された画質パラメーターに応じて明るい部分を縮小します。DILATE
— 指定された画質パラメーターに応じて明るい部分を拡大します。
フィルターのその他のモードについては、「参考文献」セクションを参照してください。
Processing では画像を合成することができます。合成は、各画像 (または必要な場合には画像の部分的領域) のピクセルごとに行われます。この機能は、Adobe® Illustrator® や Photoshop® で使用されている機能を真似たものです。
リスト 6 に、ADD
を指定して 2 つの画像を合成する操作を示します (図 3 を参照)。このリストでは、loadImage
関数によって 2 つの画像をロードした後、ADD
モードを使用して img2
を img1
に合成しています。blend
関数を呼び出す場合、対象となる合成先の画像 (img1
) に対して合成する画像 (img2
) を最初に指定します。その後に続ける 4 つのパラメーターは、合成する画像 (img2
) の x 座標、y 座標、幅、高さです。さらに、合成先の画像 (img1) の左上隅の x 座標、y 座標、幅、高さのパラメーターを続けます。そして最後に定義するのが、モード・パラメーターです。この例で要求している ADD
による合成では、dest_img_pixel += src_img_pixel*factor
(ただし、dest_img_pixel
の値が 255 を超えた場合は、dest_img_pixel
の値は 255 とする) という操作を実行しています。
リスト 6. 画像の合成
void setup() { size(237, 178); smooth(); } void draw() { PImage img1 = loadImage("alaska1.png"); PImage img2 = loadImage("alaska2.png"); img1.blend( img2, 0, 0, 237, 178, 0, 0, 237, 178, ADD ); image(img1, 0, 0); } |
この他にも、BLEND
(dest_img_pixel
の値は上限なし)、SUBTRACT
、DARKEST
(dest_img_pixel
と src_img_pixel*factor
のうち、暗い方の色を採用)、LIGHTEST
(dest_img_pixel
と src_img_pixel*factor
のうち、明るい方の色を採用)、MULTIPLY
(画像が暗くなります) をはじめ、数々の操作を行うことができます。「参考文献」セクションに合成のモードに関するリンクを記載してあります。
図 3. 合成操作の結果の画像
最後に紹介する画像処理の方法は、よりマニュアルでの操作に近い手法です。このモードでは、ピクセルごとに個別の操作をすることができます。ディスプレイ・ウィンドウは、color
型からなる 1 次元の配列で構成されているため、(リスト 7 に示されているように、background
関数を使用して) 画像を表示した後 、loadPixels
関数を使用することによって、pixels
配列に含まれるディスプレイ・ウィンドウのピクセルにアクセスすることができます。loadPixels
関数はディスプレイ・ウィンドウを pixels
配列内にロードする一方、updatePixels
関数は pixels
配列を基にディスプレイ・ウィンドウを更新します。
リスト 7. ピクセル・マップを使った画像の操作
void setup() { size(237, 178); smooth(); } void draw() { PImage img = loadImage("alaska2.png"); background(img); loadPixels(); for (int i = 0 ; i < img.width*img.height ; i++) { color p = pixels[i]; float r = red(p)/2; float g = green(p); float b = blue(p); pixels[i] = color(r, g, b); } updatePixels(); } |
ディスプレイ・ウィンドウは pixels 配列の中にありますが、これを操作するにはさまざまな手段を使うことができます。リスト 7 に示されているのは、最初に各ピクセルから color 型 (変数 p
) を作成し、それを使って表示を変更する方法です。この変数は、red
、green
、blue
関数を使用することによって、さらに RGB それぞれの値に分解することができます。上記の例では、ディスプレイ・ウィンドウに表示された画像の赤の要素の値を半分にしてから、color
関数を使ってピクセルを再ロードしています。これは、RGB それぞれの色を取得して、ピクセルを再構成する関数です。図 4 に、リスト 7 を実行する前と実行した後の画像を示します。
図 4. pixels 配列を使用したマニュアルでの画像操作
このセクションでは、Processing (具体的には OOP (Object Oriented Programming: オブジェクト指向プログラミング)) の機能のいくつかを実演するアプリケーションを紹介します。このサンプル・アプリケーションは、数値最適化および機械学習の分野で使用されるアプリケーションです。
粒子群とは、自然から発想を得た最適化手法です。この手法では、探索空間のなかで見つかった最適解 (粒子自体の最適解と大域的最適解の両方) に従って移動する解候補 (粒子) の集団を使用します。単純ながらも、探索空間の興味深い視覚表現を提供する粒子群最適化 (PSO: Particle Swarm Optimization) は、データを視覚化するための言語 (詳細は「参考文献」を参照) で探索する際に使用するには最適な手法となります。粒子群は、2 次元空間を移動して大域的最適解を探索するからです。
PSO の Processing 実装は、2 つのクラスからなります。その 1 つは個々の粒子を実装する Particle
というクラスです。Particle クラスでは、PSO に従い、各粒子はその現在の位置、現在の速度、現在の適応度、最大適応度、および粒子の最適解を維持します (リスト 8 を参照)。Particle
クラスが PSO をサポートするために提供する数々のメソッドには、コンストラクター (探索空間に粒子を不規則に配置)、適応度を計算する関数 (この例に示す calculateFitness 関数では、sombrero
関数に基づいて計算を行っており、z
が適応度を表しています)、update
関数 (粒子をその現在の速度ベクトルに基づいて移動するための関数)、そして show
関数 (粒子を探索空間に表示するための関数) があります。さらに、粒子の要素 (適応度、x 位置、y 位置) をユーザーに公開する 3 つのヘルパー関数もあります。
リスト 8. PSO の
Particle
クラスclass Particle { float locX, locY; float velX = 0.0, velY = 0.0; float fitness = 0.0; float bestFitness = -10.0; float pbestX = 0.0, pbestY = 0.0; // Best particle solution float vMax = 10.0; // Max velocity float dt = 0.1; // Used to constrain changes to each particle Particle() { locX = random( dimension ); locY = random( dimension ); } void calculateFitness() { // Clip the particles if ((locX < 0) || (locX > dimension) || (locY < 0) || (locY > dimension)) fitness = 0; else { // Calculate fitness based on the sombrero function. float x = locX - (dimension / 2); float y = locY - (dimension / 2); float r = sqrt( (x*x) + (y*y) ); fitness = (sin(r)/r); } // Maintain the best particle solution if (fitness > bestFitness) { pbestX = locX; pbestY = locY; bestFitness = fitness; } } void update( float gbestX, float gbestY, float c1, float c2 ) { // Calculate particle.x velocity and new location velX = velX + (c1 * random(1) * (gbestX - locX)) + (c2 * random(1) * (pbestX - locX)); if (velX > vMax) velX = vMax; if (velX < -vMax) velX = -vMax; locX = locX + velX*dt; // Calculate particle.y velocity and new location velY = velY + (c1 * random(1) * (gbestY - locY)) + (c2 * random(1) * (pbestY - locY)); if (velY > vMax) velY = vMax; if (velY < -vMax) velY = -vMax; locY = locY + velY*dt; } void show() { point( (int)locX, (int)locY); } float pFitness() { return fitness; } float xLocation() { return locX; } float yLocation() { return locY; } } |
もう 1 つのクラスは、粒子群を対象とした Swarm というクラスです (リスト 9 を参照)。このクラスは、Particles
の配列 (粒子群コンストラクターで作成され、初期化されたもの)、現在の大域的最適解 (x 座標と y 座標)、そして 2 つの学習因数で構成された配列を保持します。学習因数は、粒子の移動 (探索) の中心が自己最適解 (c2
) または大域的最適解 (c1
) のどちらに置かれているのかの評価基準となります。因数のそれぞれが、最適解が粒子に及ぼす影響を示します。
run
メソッドは、PSO シミュレーションにおける 1 つのステップを実行します。まず、このメソッドは粒子群に含まれる粒子ごとの適応度を計算し、それから大域的最適解を見つけます。そしてこの情報を指定して update
関数を呼び出して粒子を移動した後、show
関数を呼び出して、粒子群に含まれる各粒子の移動結果を表示します。
リスト 9. PSO の Swarm クラス
class Swarm { float gbestX = 0.0, gbestY = 0.0; // Global best solution float c1 = 0.1, c2 = 2.0; // Learning factors Particle swarm[]; Swarm() { swarm = new Particle[numParticles]; for (int i = 0 ; i < numParticles ; i++) { swarm[i] = new Particle(); } } void run() { // Calculate each particle's fitness for (int i = 0 ; i < numParticles ; i++) { swarm[i].calculateFitness(); } findGlobalBest(); // Update each particle and display it. for (int i = 0 ; i < numParticles ; i++) { swarm[i].update( gbestX, gbestY, c1, c2 ); swarm[i].show(); } } void findGlobalBest() { float fitness = -10.0; for (int i = 0 ; i < numParticles ; i++) { if (swarm[i].pFitness() > fitness) { gbestX = swarm[i].xLocation(); gbestY = swarm[i].yLocation(); fitness = swarm[i].pFitness(); } } } void showGlobalBest() { println("Best Particle Result: " + gbestX + " " + gbestY); } } |
リスト 10 に記載するのは、上記で定義したクラスを使用したユーザー・アプリケーションです。このアプリケーションは、粒子の数、ディスプレイ・ウィンドウのサイズ (dimension
)、そして粒子群自体など、PSO で構成可能な項目のいくつかを定義しています。setup
関数がウィンドウおよび色を準備すると、draw
関数が粒子群を呼び出して、大域的最適解を 10 回の繰り返しごとに出力します。このシミュレーションでは sombrero
関数を使って最適化を行うため、最適条件がディスプレイの中心となります。
リスト 10. PSO を駆動するアプリケーション
// Particle Swarm Optimization int numParticles = 200; int iteration = 0; float dimension = 500; Swarm mySwarm = new Swarm(); void setup() { background(255); fill(0); size( int(dimension), int(dimension)); smooth(); } void draw() { background(255); // remove for trails mySwarm.run(); if ((iteration++ % 10) == 0) mySwarm.showGlobalBest(); } |
以下に記載する 2 つの図に、Processing で実行した PSO シミュレーションの出力を示します。図 5 には、PSO の時間的推移を示しています。その後の図 6 に示す PSO の軌跡が、最適条件に至るまでの粒子の経路を明らかにします。
図 5. PSO シミュレーションの時間的推移
図 6 に示す軌跡からは、粒子の経路が中央の最適解に集まってきている様子が明らかに見て取れます。一部の経路はループしていることがわかりますが、これは、粒子が自己の最適解に群がってから、大域的最適解へと移動していることを示しています。
図 6. PSO シミュレーションの軌跡
Processing コードは Java 言語に変換されてから実行されます。そのため、Processing アプリケーションを Java アプレットや Java アプリケーションに変換するのは簡単です。この変換を行うには、Processing 開発環境 (PDE) で「File (ファイル)」をクリックしてから、「Export (エクスポート)」をクリックしてアプレットをエクスポートするか、「Export Application (アプリケーションのエクスポート)」をクリックして Java アプリケーションをエクスポートします。すると、sketchbook ディレクトリーにコードと、この操作に関連するファイルが生成されます。リスト 11 には、エクスポートされたアプレット (applet サブディレクトリー)、エクスポートされたアプリケーション (この特定ターゲットの 3 つの application ディレクトリー)、そしてソース自体 (pso.pde) が示されています。
リスト 11. エクスポート後の Processing の sketchbook サブディレクトリー
mtj@ubuntu:~/sketchbook/pso$ ls applet application.linux application.macosx application.windows pso.pde |
applet サブディレクトリー内にオリジナルの処理ソース、変換後の Java ソース、JAR ファイル、およびサンプル index.html ファイルが置かれているので、変換結果を確認することができます。
0 件のコメント:
コメントを投稿