小嶋秀樹 | 研究室
日本語 | English
iPhone アプリ開発道場(応用編:その1)

ここでは,画像(静止画・動画・カメラ)の扱いかたを,実例をとおして,解説していきます.下のリンクから sozai1.zip をダウンロードし,必要に応じて,プロジェクトごとの bin/data フォルダにコピーしてください.

なお,openFrameworks を使って Windows あるいは Mac のデスクトップで動作するアプリを開発したい場合は「iPhone 道場(Windows/Mac 編)」で準備をしてから,つぎの段階に進んでください.

静止画 (ofImage) の扱いかた

静止画について,ファイルからの読み込み,画面への表示,ファイルへの保存などを可能にするのが ofImage クラスです.ここでは,この ofImage クラスの使い方を説明します.

【第1段階:画像ファイルの読み込みと表示】

画像ファイル(png や jpg など)をそのまま画面に表示します.いつものように,emptyExample をコピー・名前変更します.その中にある bin/data に,sozai1.zip から image0, image1 をコピー&ペーストしてください.testApp.h と testApp.mm は,およそ以下のとおりです.

class testApp : public ofxiPhoneApp {
    private:
        ofImage image;
    public:
        (... 変更なし ...)
};
ofImage example 1
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 を変更してみてください.

ofImage example 1
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);
}

これで画像ファイルの全体が表示できましたが,こんどは縦長に引き延ばされています.ちょっとキモチ悪いので,さらに次のように変更してみます.

ofImage example 1
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回繰り返しています.

ofImage example 1
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);
        }
    }
}
【第2段階:重ね合わせ】

複数の画像を重ね合わせて描画すれば,新しく描画した画像で,それまでに描画した画像が隠され(上書きされ)ます.ためしに,カラー画像 xkozima1.png とモノクロ画像 xkozima2.png を重ね描きしてみましょう.

ofImage example 1
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%)のグレーの雲です.まずは,骨格部分として,タッチするたびに,カラー画像とモノクロ画像が切り替わるようにします.

ofImage example 1
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枚ずつ減らしていきます.なんとなくマジックのように見えませんか?

ofImage example 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枚ずつ減らしていくことで,よりリアルな雲になると思います.下記のサンプルを参考に,やってみてください.

ofImage example 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) {
    (... 変更なし ...)
}
【第3段階:移動・拡大・縮小・回転】

画像の移動・拡大・縮小なども自由にできます.まず,メソッド setAnchorPercent() によって,画像を幾何学的に操作するときの「操作中心」を,画像の中心にセットします.こうすることで,メソッド draw(x, y, width, height) は,(x, y) が画像の操作中心と重なるように,幅 width・高さ height の大きさに画像を描画できます.画像の左上端を中心とするデフォルト状態よりも便利です.たとえば,つぎの例は,タッチした位置を中心として,画像を 150×150 の大きさで描画しています.

ofImage example 1
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;
}

画像の操作中心を変えることで,拡大・縮小もその中心のまわりに適用されます.たとえば,以下のように改造することで,呼吸をするかのように画像が拡大・縮小します.

ofImage example 1
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 軸が延びる)での画像の回転を扱います.

ofImage example 1
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次元配列となります.

ofImage pixels

たとえば 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() を利用しています.

ofImage example 1
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() の呼び出し)に反映されるようになります.

ofImage wave
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 個のタップを記録し,そこから波をつくります.

ofImage waves
#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 のリファレンス を参照してください.

動画ファイルとカメラの扱いかた
【動画 (ofVideoPlayer) の扱いかた】

動画ファイルの再生も,基本的には静止画と同じです.内部での画像データは OF_IMAGE_COLOR (RGB) 形式となります.注意すべき点は,まず loadMovie() メソッドで動画ファイルを読み込み,少し間を置いてから play() メソッドで再生を開始すると,動作が安定するようです.また,testApp::update() の中では update() メソッドによって新しいフレームを取り込み,testApp::draw() の中で draw() メソッドを呼び出すようにします.

Windows 上の openFrameworks は動画再生に QuickTime ライブラリを利用しています.Window 上で動作を確認するには,あらかじめ QuickTime をインストール(無料)しておく必要があります.

つぎのプログラムは,動画ファイル test.mov を再生するものです.動画ファイルは,sozai1.zip に入っていますので,bin/data フォルダにコピーしてください.

ofVideoPlayer
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() を呼び出して,配列内容変化を表示に反映できるようにします.

ofVideoPlayer
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);
    }
}

(... 以下変更なし ...)
ofVideoPlayer

発展課題: さらに,右図のように動画再生するには,どうしたらよいでしょうか.ヒントは,image.draw() で負の高さを与えることです.

このほかにも,再生スピードや再生方向を変える,音量を設定するなど,いろいろな機能が使えます.動画ファイルを扱う ofVideoPlayer クラスの詳細については,openFrameworks のリファレンス を参照してください.

【カメラ (ofVideoGrabber) の扱いかた】

カメラからの動画取得も,基本的には静止画や動画ファイルと同じです.内部での画像データは 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 のリファレンス を参照してください.