ここでは,画像(静止画・動画・カメラ)の扱いかたを,実例をとおして,解説していきます.下のリンクから 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 のリファレンス を参照してください.