ここでは,画像(静止画・動画・カメラ)の扱いかたを,実例をとおして,解説していきます.下のリンクから sozai1.zip をダウンロードし,必要に応じて,プロジェクトごとの bin/data フォルダにコピーしてください.
- このページで利用するおもな素材:sozai1.zip
なお,openFrameworks を使って Windows あるいは Mac のデスクトップで動作するアプリを開発したい場合は「iPhone 道場(Windows/Mac 編)」で準備をしてから,つぎの段階に進んでください.
静止画について,ファイルからの読み込み,画面への表示,ファイルへの保存などを可能にするのが ofImage クラスです.ここでは,この ofImage クラスの使い方を説明します.
画像ファイル(png や jpg など)をそのまま画面に表示します.いつものように,emptyExample をコピー・名前変更します.その中にある bin/data に,sozai1.zip から image0, image1 をコピー&ペーストしてください.testApp.h と testApp.mm は,およそ以下のとおりです.
class testApp : public ofxiPhoneApp {
private:
ofImage image;
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofBackground(127,127,127); // load image image.loadImage("xkozima1.png"); } void testApp::update() {} void testApp::draw() { image.draw(0, 0); }
上の例では,画像ファイル xkozima1.png(500×500 ピクセル)と比べると,こじま先生の顔が切れてしまっています.またキーポンもほとんど見えていません.そこで,iPhone の画面に合わせて,大きさを変えて表示するようにします.以下のように,testApp.mm を変更してみてください.
void testApp::setup() {
ofRegisterTouchEvents(this);
ofxAccelerometer.setup();
ofxiPhoneAlerts.addListener(this);
ofBackground(127,127,127);
// load image
image.loadImage("xkozima1.png");
}
void testApp::update() {}
void testApp::draw() {
image.draw(0, 0, 320, 480);
}
これで画像ファイルの全体が表示できましたが,こんどは縦長に引き延ばされています.ちょっとキモチ悪いので,さらに次のように変更してみます.
void testApp::setup() {
ofRegisterTouchEvents(this);
ofxAccelerometer.setup();
ofxiPhoneAlerts.addListener(this);
ofBackground(127,127,127);
// load image
image.loadImage("xkozima1.png");
}
void testApp::update() {}
void testApp::draw() {
image.draw(0, 0, 320, 320);
}
画像をアイコン状にして,たくさん並べてみましょう.ここでは元の画像を 80×80 の大きさに描画し,それを縦横に 6×4 = 24回繰り返しています.
void testApp::setup() {
ofRegisterTouchEvents(this);
ofxAccelerometer.setup();
ofxiPhoneAlerts.addListener(this);
ofBackground(127,127,127);
// load image
image.loadImage("xkozima1.png");
}
void testApp::update() {}
void testApp::draw() {
for (int y = 0; y < 6; y++) {
float posY = y * 80;
for (int x = 0; x < 4; x++) {
float posX = x * 80;
image.draw(posX, posY, 80, 80);
}
}
}
複数の画像を重ね合わせて描画すれば,新しく描画した画像で,それまでに描画した画像が隠され(上書きされ)ます.ためしに,カラー画像 xkozima1.png とモノクロ画像 xkozima2.png を重ね描きしてみましょう.
class testApp : public ofxiPhoneApp {
private:
ofImage image1, image2;
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofBackground(127,127,127); // load image image1.loadImage("xkozima1.png"); image2.loadImage("xkozima2.png"); } void testApp::update() {} void testApp::draw() { image1.draw(0, 0, 200, 200); image2.draw(120, 140, 200, 200); image1.draw(60, 280, 200, 200); }
つぎに,透明度をもった画像の重ね合わせについて説明します.透明度をもった画像は,たとえば PowerPoint や Illustrator などで作成することができます.ここでは,PowerPoint で作成した smoke.png という画像ファイルを使います.この smoke.png は,透明度 90%(つまり不透明度 10%)のグレーの雲です.まずは,骨格部分として,タッチするたびに,カラー画像とモノクロ画像が切り替わるようにします.
class testApp : public ofxiPhoneApp {
private:
ofImage image1, image2;
bool magicOn;
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofBackground(127,127,127); // load image image1.loadImage("xkozima1.png"); image2.loadImage("xkozima2.png"); magicOn = false; } void testApp::update() {} void testApp::draw() { // draw image if (magicOn == false) image1.draw(40, 120, 240, 240); else image2.draw(40, 120, 240, 240); } void testApp::touchDown(ofTouchEventArgs &touch) { // toggle magicOn if (magicOn) { magicOn = false; } else { magicOn = true; } }
ここに透明度をもった雲 smoke.png を重ね描きしてみましょう.testApp::setup() のなかに,ofEnableAlphaBlending() を加えてください.透明度をもった画像を描画することができるようになります.ここでは,同じ位置に 15 枚の雲を重ね描きし,1枚ずつ減らしていきます.なんとなくマジックのように見えませんか?
class testApp : public ofxiPhoneApp {
private:
ofImage image1, image2, imageS;
bool magicOn;
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofBackground(127,127,127); ofSetFrameRate(30); ofEnableAlphaBlending(); // load image image1.loadImage("xkozima1.png"); image2.loadImage("xkozima2.png"); imageS.loadImage("smoke.png"); // flag for magic magicOn = false; } void testApp::update() {} void testApp::draw() { // draw image if (magicOn == false) image1.draw(40, 120, 240, 240); else image2.draw(40, 120, 240, 240); // draw smoke if (smokeNum > 0) { for (int i = 0; i < smokeNum; i++) imageS.draw(0, 0, 320, 480); smokeNum--; } } void testApp::touchDown(ofTouchEventArgs &touch) { // toggle magicOn if (magicOn) { magicOn = false; smokeNum = 15; } else { magicOn = true; smokeNum = 15; } }
発展課題: 15枚の雲を少しずらして描画し,1枚ずつ減らしていくことで,よりリアルな雲になると思います.下記のサンプルを参考に,やってみてください.
class testApp : public ofxiPhoneApp {
private:
ofImage image1, image2, imageS;
bool magicOn;
float x[15], y[15];
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofBackground(127,127,127); ofSetFrameRate(30); ofEnableAlphaBlending(); // load image image1.loadImage("xkozima1.png"); image2.loadImage("xkozima2.png"); imageS.loadImage("smoke.png"); // flag for magic magicOn = false; // smoke positions for (int i = 0; i < 15; i++) { x[i] = ofRandom(-50, 50); y[i] = ofRandom(-50, 50); } } void testApp::update() {} void testApp::draw() { // draw image if (magicOn == false) image1.draw(40, 120, 240, 240); else image2.draw(40, 120, 240, 240); // draw smoke if (smokeNum > 0) { for (int i = 0; i < smokeNum; i++) imageS.draw(x[i], y[i], 320, 480); smokeNum--; } } void testApp::touchDown(ofTouchEventArgs &touch) { (... 変更なし ...) }
画像の移動・拡大・縮小なども自由にできます.まず,メソッド setAnchorPercent() によって,画像を幾何学的に操作するときの「操作中心」を,画像の中心にセットします.こうすることで,メソッド draw(x, y, width, height) は,(x, y) が画像の操作中心と重なるように,幅 width・高さ height の大きさに画像を描画できます.画像の左上端を中心とするデフォルト状態よりも便利です.たとえば,つぎの例は,タッチした位置を中心として,画像を 150×150 の大きさで描画しています.
class testApp : public ofxiPhoneApp {
private:
ofImage image;
float posX, posY;
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofBackground(127,127,127); // load image image.loadImage("xkozima1.png"); image.setAnchorPercent(0.5, 0.5); posX = 160; posY = 240; } void testApp::update() { } void testApp::draw() { image.draw(posX, posY, 150, 150); } void testApp::touchDown(ofTouchEventArgs &touch) { posX = touch.x; posY = touch.y; } void testApp::touchMoved(ofTouchEventArgs &touch) { posX = touch.x; posY = touch.y; }
画像の操作中心を変えることで,拡大・縮小もその中心のまわりに適用されます.たとえば,以下のように改造することで,呼吸をするかのように画像が拡大・縮小します.
class testApp : public ofxiPhoneApp {
private:
ofImage image;
int posX, posY;
float rad;
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofBackground(127,127,127); ofSetFrameRate(30); // load image image.loadImage("xkozima1.png"); image.setAnchorPercent(0.5, 0.5); posX = 160; posY = 240; rad = 0; } void testApp::update() { rad += 0.1; } void testApp::draw() { float scale = 1.0 + 0.2 * sin(rad); image.draw(posX, posY, 150 * scale, 150 * scale); } (... 以下変更なし ...)
画像の回転まで含めた幾何変換を実現するには「実践編」で扱った座標変換が必要となります.ここでは,その一例として,標準座標系(原点が中央で,右向きに X 軸,上向きに Y 軸,手前に Z 軸が延びる)での画像の回転を扱います.
class testApp : public ofxiPhoneApp { private: ofImage image; float posX, posY; float rad; public: (... 変更なし ...) };
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofBackground(127,127,127); ofSetFrameRate(30); // load image image.loadImage("xkozima1.png"); image.setAnchorPercent(0.5, 0.5); posX = 160; posY = 240; rad = 0; } void testApp::update() { rad += 0.1; } void testApp::draw() { // setup standard coordinate system ofTranslate(160, 239, 0); ofScale(1, -1, 1); // translate, rotate, scale, and draw ofTranslate(posX, posY, 0); float deg = rad / PI * 180.0; ofRotateZ(deg); float scale = 1.0 + 0.2 * sin(rad); image.draw(0, 0, 150 * scale, -150 * scale); } void testApp::touchDown(ofTouchEventArgs &touch) { posX = touch.x - 160; posY = 239 - touch.y; } void testApp::touchMoved(ofTouchEventArgs &touch) { posX = touch.x - 160; posY = 239 - touch.y; }
標準座標系とタッチ座標系の対応に注意してください.また,標準座標系では Y 軸が上下反転していますので,画像を描画するときも(負の高さを与えることで)上下反転させて描画します.
画面は2次元ですが,画像データは,最上段の水平ライン(左から右へ)から各段ずつピクセルを連結した1次元配列となります.各ピクセルは,グレースケール画像であれば unsinged char 型 (1 Byte = 8 bit : 値の範囲は 0〜255) となります.0 は黒,255 は白です.カラー画像の場合は,1つのピクセルは R, G, B, に対応する unsigned char 型のデータ3つからなり,画像データは,この3つ組がピクセルの数だけ繰り返された1次元配列となります.また,アルファチャネル付きのカラー画像は,R, G, B, A の4つ組からなる1次元配列となります.
たとえば 4×3 ピクセルの画像では,グレースケール画像であれば,12 要素(12 Byte)のデータ,カラー画像であれば 36 要素(36 Byte)のデータ,アルファチャネル付きカラー画像であれば 48 要素(48 Byte)のデータとなります.
画像(ofImage クラスのインスタンス)から画像データを取り出すには,getPixels() メソッドを使います.画像のメンバ変数 type の値から,グレースケール画像 (OF_IMAGE_GRAYSCALE),カラー画像 (OF_IMAGE_COLOR, RGB 形式),アルファチャネル付きカラー画像 (OF_IMAGE_COLOR_ALPHA, RGBA 形式) のいずれであるかを調べることができます.
つぎのプログラムは,画像をクリックすることで,その位置の色情報を画面表示するものです.画面に文字を描画するために,ofDrawBitmapString() を利用しています.
class testApp : public ofxiPhoneApp {
private:
ofImage image;
int type, width, height;
unsigned char *data;
int posX, posY;
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofBackground(127,127,127); // load image image.loadImage("xkozima-small.png"); type = image.type; width = image.width; height = image.height; data = image.getPixels(); } void testApp::update() {} void testApp::draw() { // draw image image.draw(0, 0); // report pixel data char buf[100]; if (0 <= posX && posX < width && 0 <= posY && posY < height ) { // mouse is inside of the image switch (type) { case OF_IMAGE_GRAYSCALE: sprintf(buf, "(%d, %d) = GRAY %d", posX, posY, data[posY * imageWidth + posX] ); break; case OF_IMAGE_COLOR: sprintf(buf, "(%d, %d) = RGB (%d, %d ,%d)", posX, posY, data[(posY * width + posX) * 3], data[(posY * width + posX) * 3 + 1], data[(posY * width + posX) * 3 + 2] ); break; case OF_IMAGE_COLOR_ALPHA: sprintf(buf, "(%d, %d) = RGBA (%d, %d ,%d, %d)", posX, posY, data[(posY * width + posX) * 4], data[(posY * width + posX) * 4 + 1], data[(posY * width + posX) * 4 + 2], data[(posY * width + posX) * 4 + 3] ); break; } } else { // mouse is outside of the image sprintf(buf, "out of range"); } ofSetColor(255, 255, 255); ofDrawBitmapString(buf, 10, 320); } void testApp::touchDown(ofTouchEventArgs &touch) { posX = touch.x; posY = touch.y; } void testApp::touchMoved(ofTouchEventArgs &touch) { posX = touch.x; posY = touch.y; }
画像データ(ピクセルの値)を書き換えることで,画像を生成(あるいは変更)することができます.ただし,画像データを書き換えた後で,update() メソッドを呼び出すことが必要です.これで,画像データが画面表示(draw() の呼び出し)に反映されるようになります.
class testApp : public ofxiPhoneApp {
private:
ofImage image;
unsigned char *data;
float rad;
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofSetFrameRate(30); // setup image image.allocate(320, 480, OF_IMAGE_GRAYSCALE ); data = image.getPixels(); // radian rad = 0; } void testApp::update() { for (int y = 0; y < 480; y++) { int dy = y - 240; for (int x = 0; x < 320; x++) { int dx = x - 160; float dist = sqrt((float) (dx * dx + dy * dy)); data[y * 320 + x] = 128 + 64 * sin(-rad + dist / 10.0); } } image.update(); rad += 0.1; } void testApp::draw() { image.draw(0, 0); }
発展課題: タップした場所から波紋が広がるように改造してみます.最新の 7 個のタップを記録し,そこから波をつくります.
#define TAP_MAX 7
class testApp : public ofxiPhoneApp {
private:
ofImage image;
unsigned char *data;
float rad;
int tapCount;
int tapX[TAP_MAX], tapY[TAP_MAX];
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); ofSetFrameRate(30); // setup image image.allocate(320, 480, OF_IMAGE_GRAYSCALE ); data = image.getPixels(); // radian rad = 0; // taps for (int k = 0; k < TAP_MAX; k++) { tapX[k] = 160; tapY[k] = 240; } tapCount = 0; } void testApp::update() { for (int y = 0; y < 480; y++) { for (int x = 0; x < 320; x++) { float value = 128; for (int k = 0; k < TAP_MAX; k++) { int dx = x - tapX[k]; int dy = y - tapY[k]; float dist = sqrt((float) (dx * dx + dy * dy)); value += (100.0 / TAP_MAX) * sin(-rad + dist / 10.0); } data[y * 320 + x] = value; } } image.update(); rad += 0.1; } void testApp::draw() { image.draw(0, 0); } void testApp::touchDown(ofTouchEventArgs &touch) { tapX[tapCount % TAP_MAX] = touch.x; tapY[tapCount % TAP_MAX] = touch.y; tapCount++; }
発展課題: この複雑な波紋を,カラー画像で出せると面白いと思います.ぜひチャレンジしてみてください.
静止画を扱う ofImage クラスの詳細については,openFrameworks のリファレンス を参照してください.
動画ファイルの再生も,基本的には静止画と同じです.内部での画像データは OF_IMAGE_COLOR (RGB) 形式となります.注意すべき点は,まず loadMovie() メソッドで動画ファイルを読み込み,少し間を置いてから play() メソッドで再生を開始すると,動作が安定するようです.また,testApp::update() の中では update() メソッドによって新しいフレームを取り込み,testApp::draw() の中で draw() メソッドを呼び出すようにします.
Windows 上の openFrameworks は動画再生に QuickTime ライブラリを利用しています.Window 上で動作を確認するには,あらかじめ QuickTime をインストール(無料)しておく必要があります.
つぎのプログラムは,動画ファイル test.mov を再生するものです.動画ファイルは,sozai1.zip に入っていますので,bin/data フォルダにコピーしてください.
class testApp : public ofxiPhoneApp {
private:
ofVideoPlayer player;
bool isPlaying;
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); // start player player.loadMovie("test.mov"); isPlaying = false; } void testApp::update() { if (isPlaying) player.update(); } void testApp::draw() { if (isPlaying) player.draw(0, 0, 320, 240); } void testApp::touchDown(ofTouchEventArgs &touch) { if (! isPlaying) { player.play(); isPlaying = true; } }
少しだけ改造して,画面をタッチしている間だけ動画ポーズするようにします.ポーズするには,setPaused(true) メソッドを呼び出します.false を与えればポーズは解除されます.
void testApp::touchDown(ofTouchEventArgs &touch) { if (! isPlaying) { player.play(); isPlaying = true; } else { player.setPaused(true); } } void testApp::touchUp(ofTouchEventArgs &touch) { if (isPlaying) { player.setPaused(false); } }
発展課題: さらに少しだけ改造して,動画のフレームごとに getPixels() によって画像データを取り出し,色反転した画像(ネガ)に変換して image(ofImage クラスのインスタンス)に格納しています.具体的には,testApp::update() の中で,player.update() によって1フレームを取り込み,その画像データを player.getPixels() によって取り出し,それを image.setFromPixels によってコピーします.つぎに,image.getPixels() によって画像データ(配列)を取り出し,その内容をすべて反転(255 から減算)しています.最後に image.update() を呼び出して,配列内容変化を表示に反映できるようにします.
class testApp : public ofxiPhoneApp {
private:
ofVideoPlayer player;
bool isPlaying;
ofImage image;
public:
(... 変更なし ...)
};
void testApp::setup() { (... 変更なし ...) } void testApp::update() { if (isPlaying) { player.update(); // copy to image image.setFromPixels(player.getPixels(), player.getWidth(), player.getHeight(), OF_IMAGE_COLOR ); // get pixel data of the image unsigned char *data = image.getPixels(); // make it negative int bytes = image.getWidth() * image.getHeight() * 3; for (int k = 0; k < bytes; k++) data[k] = 255 - data[k]; // must update after pixel manipulations image.update(); } } void testApp::draw() { if (isPlaying) { player.draw(0, 0, 320, 240); image.draw(0, 240, 320, 240); } } (... 以下変更なし ...)
発展課題: さらに,右図のように動画再生するには,どうしたらよいでしょうか.ヒントは,image.draw() で負の高さを与えることです.
このほかにも,再生スピードや再生方向を変える,音量を設定するなど,いろいろな機能が使えます.動画ファイルを扱う ofVideoPlayer クラスの詳細については,openFrameworks のリファレンス を参照してください.
カメラからの動画取得も,基本的には静止画や動画ファイルと同じです.内部での画像データは OF_IMAGE_COLOR (RGB) 形式となります.ちなみに,iPhone 4 には背面と前面にそれぞれカメラがありますが,これらは setDeviceID() メソッドで選択できます.カメラを切り替えるには,いちど close() してから setDeviceID() でカメラを指定し,再び initGrabber() を呼び出してください.
以下のプログラムを実行するには,iPhone 実機をターゲットとしてください.Mac や Windows マシンのウェブカム(あるいは外付けウェブカム)を利用する場合は,initGrabber(640, 480) のような横長のピクセル数で initGrabber() を呼び出してください.
class testApp : public ofxiPhoneApp {
private:
ofVideoGrabber grabber;
public:
(... 変更なし ...)
};
void testApp::setup() { ofRegisterTouchEvents(this); ofxAccelerometer.setup(); ofxiPhoneAlerts.addListener(this); // grabber grabber.setDeviceID(0); grabber.initGrabber(320, 480); } void testApp::update() { grabber.grabFrame(); } void testApp::draw() { grabber.draw(0, 0); }
カメラを扱う ofVideoGrabber クラスの詳細については,openFrameworks のリファレンス を参照してください.