小嶋秀樹 | 研究室
日本語 | English
openFrameworks 応用編(1)

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

静止画(ofImage)の扱いかた

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

【画像ファイルの読み込みと表示】

画像ファイル(png や jpg など)をそのまま画面に表示します.いつものように,emptyExample をコピーし,dojoExample7 に名前を変更します.その中にある bin > data フォルダに,ofSozai1.zip から earth.jpg をコピーしてください.この JPEG ファイルは 750x750 の大きさをもつ地球の画像です.(bin/data フォルダがない場合は,bin フォルダとその中に data フォルダを新規作成してください.)

画像ファイルを読み込み,ウィンドウに表示するプログラム(ofApp.cpp, ofApp.h)は,およそ以下のようになります.image は ofImage クラスのインスタンス(ゴム印で押したメモリ領域=オブジェクト)です.

class ofApp : public ofBaseApp {
private:
    ofImage image;
public:
    (... 変更なし ...)
};
ofImage example 1
void ofApp::setup () {
    //  600x400, 30 frames/sec
    ofSetWindowShape(600, 400);
    ofSetFrameRate(30);
    //  set background
    ofSetBackgroundColor(0, 80, 0);
    //  load image
    image.load("earth.jpg");
}
void ofApp::update () {
}
void ofApp::draw () {
    image.draw(0, 0);
}
(... 以下変更なし ...)

オリジナルの画像は地球全体が正方形(750x750 ピクセル)の領域に収まっているはずですが,600x400 のウィンドウにはその左上の部分だけが表示されています.そこで,つぎのように ofApp.cpp を変更すれば,600x400 のウィンドウ領域に,元画像の全体が表示されます.image.draw(x, y) の場合は,画像の左上を,現在の座標系の (x, y) に合わせた位置に,画像をその大きさを変えずに表示させますが,image.draw(x, y, w, h) とした場合は,画像の大きさを横幅 w,縦高 h に調整して表示させるわけです.

ofImage example 1
void ofApp::setup () {
    (... 変更なし ...)
}
void ofApp::update () {
}
void ofApp::draw () {
    image.draw(0, 0, 600, 400);
}
(... 以下変更なし ...)

全体が表示されましたが,横に伸びた地球になってしまいました.これを補正しつつ,全体を表示するにはどうすればよいでしょうか.たとえば,以下のように変更すれば解決できます.

ofImage example 1
void ofApp::setup () {
    (... 変更なし ...)
}
void ofApp::update () {
}
void ofApp::draw () {
    image.draw(100, 0, 400, 400);
}
(... 以下変更なし ...)

また,以下のように,image.setAnchorPercent(px, py) を与えることで,描画するときの「アンカー位置」(デフォルトでは画像左上端)を認知の位置に移動させることができます.image.setAnchorPercent(0.5, 0.5) とすれば画像の中央がアンカー位置となります.image.draw(x, y) は,このアンカー位置が現在の座標系の (x, y) に来るように描画します.image.draw(x, y, w, h) も同様です.(この image.setAnchorPercent() 呼び出しは ofApp::setup() の中に移しても構いません.)

void ofApp::draw () {
    image.setAnchorPercent(0.5, 0.5);
    image.draw(300, 200, 400, 400);
}
(... 以下変更なし ...)
ofImage example 1

発展課題1: 単に,左右反転・上下反転だけであれば座標変換は不要です.上の例であれば,image.draw(300, 200, -400, 400) とすれば左右反転となります.

発展課題2: 画像も ofRect() や ofCircle() と同じように,それを描画する座標系を変換することで,平行移動や回転,拡大縮小などが自由にできます.たとえば,つぎのようにすれば,地球を回転できます.image.draw() の縦高・横幅には (400, -400) を与えて,上下反転させた描画としている点に注意してください.

class ofApp : public ofBaseApp {
private:
    ofImage image;
    float deg;
public:
    (... 変更なし ...)
};
ofImage example 1
void ofApp::setup () {
    (... 変更なし ...)
    image.load("earth.jpg");
    image.setAnchorPercent(0.5, 0.5);
    deg = 0.0;    
}
void ofApp::update () {
    deg += 0.1;
}
void ofApp::draw () {
    //  setup standard coordinate system
    ofTranslate(300, 200);
    ofScale(1, -1);
    //  rotate around the origin
    ofRotateZ(deg);
    image.draw(0, 0, 400, -400);
}
(... 以下変更なし ...)

発展課題3: ウィンドウがリサイズされても追従できるように改良しましょう.プログラムの動作がわかりやすいように背景を深緑色にしてあります.ofGetWindowWidth() や image.getHeight() などの意味は自明ですね.

ofImage example 1
void ofApp::setup () {
    (... 変更なし ...)
    image.load("earth.jpg");
    image.setAnchorPercent(0.5, 0.5);
}
void ofApp::update () {
}
void ofApp::draw () {
    //  get size
    float winW = ofGetWindowWidth(), 
          winH = ofGetWindowHeight(),
          imgW = image.getWidth(),
          imgH = image.getHeight();
    //  fit it
    float ratioW = winW / imgW, 
          ratioH = winH / imgH, 
          ratioMin;
    if (ratioW < ratioH) 
        ratioMin = ratioW;
    else
        ratioMin = ratioH;
    //  draw it
    image.draw(winW/2, winH/2, ratioMin*imgW, ratioMin*imgH);
}
(... 以下変更なし ...)

なお,パソコンの画面サイズを知りたい場合は,ofGetScreenWidth(), ofGetScreenHeight() が使えます。(ofGetScreenWidth/Height() と ofGetWindowWidth/Height() は整数を返します。一方,ofImage クラスのメソッドである image.getWidth/Height() は実数 (float) を返します。)

発展課題4: 画像サイズは読み込み時に調べれば,その後は不変です.また,比率(ratioMin)の計算は,ウィンドウをリサイズしたときのみ実施すれば十分ですから,対応するイベントハンドラである ofApp::windowResized() に入れます.このあたりを整理すると,つぎのようなプログラムになります.

class ofApp : public ofBaseApp {
private:
    ofImage image;
    float winW, winH, imgW, imgH, ratioMin;
public:
    (... 変更なし ...)
};
ofImage example 1
void ofApp::setup () {
    winW = 600;
    winH = 400;
    //  600x400, 30 frames/sec
    ofSetWindowShape(winW, winH);
    ofSetFrameRate(30);
    //  set background
    ofSetBackgroundColor(0, 80, 0);
    //  load image
    image.load("earth.jpg");
    image.setAnchorPercent(0.5, 0.5);
    imgW = image.getWidth();
    imgH = image.getHeight();
}
void ofApp::update () {
}
void ofApp::draw () {
    //  draw it
    image.draw(winW/2, winH/2, ratioMin*imgW, ratioMin*imgH);
}
void ofApp::windowResize (int w, int h)
{
    //  get size
    winW = w;
    winH = h;
    //  fit it
    float ratioW = winW / imgW, 
          ratioH = winH / imgH;
    if (ratioW < ratioH) 
        ratioMin = ratioW;
    else
        ratioMin = ratioH;
}
(... 以下変更なし ...)
【画像の重ね合わせ】

夜空の画像(back)に星の画像を重ねて描画します.単純に考えると,まず夜空を描画し,その上(次)に,星を描画すればよいでしょう.プログラムを書くと,つぎのようになります.新たに dojoExample8 をつくり,bin/data に画像ファイル sky1.jpg と star.png をコピーしてから動かしてください.

class ofApp : public ofBaseApp {
private:
    ofImage sky1, star;
public:
    (... 変更なし ...)
};
ofImage example 1
void ofApp::setup () {
    //  600x400, 30 frames/sec
    ofSetWindowShape(600, 400);
    ofSetFrameRate(30);
    //  black background
    ofSetBackgroundColor(0, 0, 0);
    //  load image
    sky1.load("sky1.jpg");
    star.load("star.png");
}
void ofApp::update () {
}
void ofApp::draw () {
    //  draw sky
    sky1.draw(0, 0, 600, 400);
    //  draw star
    star.draw(300, 100, 100, 100);
}
(... 以下変更なし ...)

たしかに夜空に星が描かれましたが,残念なことに,星の画像の黒い背景部分まで描かれてしまいました.これを回避するには,アルファブレンディングを行ないます.まず,背景を透明にした星の画像を作ります.これには Gimp や Inkscape といったフリーソフトや,Illustrator のような商用ソフトを使うとよいでしょう.左下は黒背景の star.png,右下は透明背景の starAlpha.png です.

of-star.png of-starAlpha.png

この真上に starAlpha.png を描画すると,こうなります.

of-starAlpha.png

上のプログラムで,ofApp::setup() に ofEnableAlphaBlending() を加えて,アルファブレンディング(透明度をもった画像の描画)を可能にし,star.png を starAlpha.png に置き換えます.画像ファイル starAlpha.png を bin/data に入れて,動作を確かめてください.つぎのような美しい画像が得られるはずです.

ofImage example 1
void ofApp::setup () {
    //  600x400, 30 frames/sec
    ofSetWindowShape(600, 400);
    ofSetFrameRate(30);
    ofEnableAlphaBlending();
    //  black background
    ofSetBackgroundColor(0, 0, 0);
    //  load image
    sky1.load("sky1.jpg");
    star.load("starAlpha.png");
}

発展課題1: この星をゆっくりと明滅させます.いつものように,内部変数 rad と sin() を使います.

class ofApp : public ofBaseApp {
private:
    ofImage sky1, star;
    float rad;
public:
    (... 変更なし ...)
};
void ofApp::setup () {
    //  600x400, 30 frames/sec
    ofSetWindowShape(600, 400);
    ofSetFrameRate(30);
    ofEnableAlphaBlending();
    //  black background
    ofSetBackgroundColor(0, 0, 0);
    //  load image
    sky1.load("sky1.jpg");
    star.load("starAlpha.png");
    //  init vars
    rad = 0;
}
void ofApp::update () {
    rad += 0.1;
}
void ofApp::draw () {
    //  draw sky
    ofSetColor(255, 255, 255, 255);
    sky1.draw(0, 0, 600, 400);
    //  draw star
    float alpha = 255 * (0.25 * sin(rad) + 0.75);
    ofSetColor(255, 255, 255, alpha);
    star.draw(300, 100, 100, 100);
}
(... 以下変更なし ...)

ofSetColor(r, g, b) は,画像に対しては照明光を設定します.画像の各ピクセルの色は,この照明光と「乗算」された色として表示されます.ofSetColor(r, g, b, a) のように第4引数としてアルファ値を与えた場合は,さらに不透明度を表わすアルファ値(アルファチャネルのない画像は 255 を想定)についても同様に「乗算」した値によって表示されます.ここでは,アルファを 0.5〜1.0 に変化させて星を描画しています.また背景については,照明光を標準(r, g, b, a) = (255, 255, 255, 255) にリセットして描画するようにしています.

発展課題2: 「流れ星」をつくります.マウスをクリックすると流れるようにしましょう.

class ofApp : public ofBaseApp {
private:
    ofImage sky1, star;
    float posX, posY;
public:
    (... 変更なし ...)
};
void ofApp::setup () {
    //  600x400, 30 frames/sec
    ofSetWindowShape(600, 400);
    ofSetFrameRate(30);
    //  black background
    ofSetBackgroundColor(0, 0, 0);
    //  load image
    sky1.load("sky1.jpg");
    star.load("starAlpha.png");
    //  init vars
    posX = -200; posY = 0;
}
void ofApp::update () {
    posX -= 10;
    posY += 2;
}
void ofApp::draw () {
    //  draw sky
    sky1.draw(0, 0, 600, 400);
    //  draw star
    star.draw(posX, posY, 50, 50);
}
void mousePressed (int x, int y, int botton) {
    posX = 600;
    posY = 50;
}
(... 以下変更なし ...)

発展課題3: 星は置いといて,夜空を変化させてみます.sky1.jpg は藍色の夜空,sky2.jpg は茜色の夜空です.これを sin(), ofSetColor(r, g, b, a) で実現します.画像ファイル sky2.jpg も bin/data に入れておきます.

class ofApp : public ofBaseApp {
private:
    ofImage sky1, sky2, star;
    float rad;
public:
    (... 変更なし ...)
};
ofImage example 1
ofImage example 1
ofImage example 1
void ofApp::setup () {
    //  600x400, 30 frames/sec
    ofSetWindowShape(600, 400);
    ofSetFrameRate(30);
    ofEnableAlphaBlending();
    //  black background
    ofSetBackgroundColor(0, 0, 0);
    //  load image
    sky1.load("sky1.jpg");
    sky2.load("sky2.jpg");
    star.load("starAlpha.png");
    //  init vars
    rad = 0;
}
void ofApp::update () {
    rad += 0.1;
}
void ofApp::draw () {
    //  draw sky
    float val, alpha1, alpha2;
    val = 0.5 * sin(rad * 0.1) + 0.5;
    alpha1 = 255 * val
    alpha2 = 255 * (1.0 - val);
    ofSetColor(255, 255, 255, alpha1);
    sky1.draw(0, 0, 600, 400);
    ofSetColor(255, 255, 255, alpha2);
    sky2.draw(0, 0, 600, 400);
    //  draw star
    float alpha = 255 * (0.25 * sin(rad) + 0.75);
    ofSetColor(255, 255, 255, alpha);
    star.draw(300, 100, 100, 100);
}
(... 以下変更なし ...)
画像の内部表現

画面は2次元ですが,画像データは,最上段の水平ライン(左から右へ)から各段ずつピクセルを連結した1次元配列となります.各ピクセルは,通常のカラー画像であれば,1つのピクセルは R, G, B, に対応する unsigned char 型 (1 Byte = 8 bit : 値の範囲は 0〜255) のデータ3つからなり,画像データは,この3つ組がピクセルの数だけ繰り返された1次元配列となります.アルファチャネル付きのカラー画像は,R, G, B, A の4つ組からなる1次元配列となります.また,この道場では扱いませんが,グレースケール画像であれば unsinged char 型の1次元配列となります.0 は黒,255 は白です.

ofImage pixels

たとえば 4×3 ピクセルの画像では,グレースケール画像であれば,12 要素(12 Byte)のデータ,カラー画像であれば 36 要素(36 Byte)のデータ,アルファチャネル付きカラー画像であれば 48 要素(48 Byte)のデータとなります.

【画像タイプを調べる】

画像タイプを調べるには,画像オブジェクト image のメソッド image.getImageType() の値を調べればよいです.その値から,グレースケール画像 (OF_IMAGE_GRAYSCALE),カラー画像 (OF_IMAGE_COLOR, RGB 形式),アルファチャネル付きカラー画像 (OF_IMAGE_COLOR_ALPHA, RGBA 形式) のいずれであるかを知ることができます.

つぎのプログラムは,画像タイプをコンソールに表示するものです.dojoExample9 をつくり,bin/data に画像ファイル(star.png, starAlpha.png, sky1.jpg, color.jpg)をコピーして,それぞれの画像ファイルについて,その画像タイプを調べてください.出力はコンソールにでます.Windows の VC++ であれば,プログラムを起動すると,黒い横長長方形の画面が一緒に出ていました.これがコンソールで,プログラムから printf() などで出力した文字列は,ここに表示されます.Mac OS の Xcode の場合は,メニュー > Run > Console を開いてください.

図
図
class ofApp : public ofBaseApp {
private:
    ofImage image;
    int imgW, imgH;
    int type;
public:
    (... 変更なし ...)
};
void ofApp::setup() {
    image.load("star.png");
    imgW = image.getWidth();
    imgH = image.getHeight();
    type = image.getImageType();
    //  setup window
    ofSetWindowShape(imgW, imgH);
    ofSetFrameRate(30);
    ofEnableAlphaBlending();
    ofSetBackgroundColor(0, 80, 0);
    //  init var
    switch (type) {
    case OF_IMAGE_GRAYSCALE:
        printf("GRAY %dx%d\n", imgW, imgH);
        break;
    case OF_IMAGE_COLOR:
        printf("RGB  %dx%d\n", imgW, imgH);
        break;
    case OF_IMAGE_COLOR_ALPHA:
        printf("RGBA %dx%d\n", imgW, imgH);
        break;
    }
}
void ofApp::update() {
}
void ofApp::draw() {
    //  draw image
    image.draw(0, 0);
}

コンソール出力は,デバッギング(プログラムが正しく動作するように修正すること)に利用できます.プログラムの動作をトレースするために,適宜 printf() を入れて,メッセージを出力させます.

【画像データを読み出す】

画像(ofImage クラスのインスタンス)から画像データを取り出すには,getPixels().getData() を使います.まず,getPixels() で ofPixels 型のデータ(幅・高さ・種類・ピクセルデータ)を取り出し,さらにその内部にあるピクセルデータを getData() を取り出しています.getData() が返す値のは,ピクセルの1次元配列(unsigned char の配列)です.あるいは,この配列の先頭要素へのポインタと言い換えてもよいです.

ofImage pixels

つぎのプログラムは,画像をクリックすることで,その位置の色情報を画面表示するものです.画面に文字を描画するために,ofDrawBitmapString(buf, x, y) を利用しています.buf は char の配列(またはその先頭要素へのポインタ)です.

図
図
class ofApp : public ofBaseApp {
private:
    ofImage image;
    int imgW, imgH;
    int type;
    char buf[100];
public:
    (... 変更なし ...)
};
void ofApp::setup () {
    image.load("color.png");
    imgW = image.getWidth();
    imgH = image.getHeight();
    type = image.getImageType();
    //  setup window
    ofSetWindowShape(imgW, imgH + 40);
    ofSetFrameRate(30);
    ofEnableAlphaBlending();
    ofSetBackgroundColor(0, 80, 0);
    //  init var
    switch (type) {
    case OF_IMAGE_GRAYSCALE:
        printf("GRAY %dx%d\n", imgW, imgH);
        break;
    case OF_IMAGE_COLOR:
        printf("RGB  %dx%d\n", imgW, imgH);
        break;
    case OF_IMAGE_COLOR_ALPHA:
        printf("RGBA %dx%d\n", imgW, imgH);
        break;
    }
    strcpy(buf, "click to check");
}
void ofApp::update () {
}
void ofApp::draw () {
    //  draw image
    image.draw(0, 0);
    //  print data
    ofDrawBitmapString(buf, 20, imgH + 20);
}
void ofApp::mousePressed (int x, int y, int button) {
    if (y >= imgH) return;
    int index = y * imgW + x;
    int valR, valG, valB, valA;
    switch (type) {
    case OF_IMAGE_GRAYSCALE:
        valA = image.getPixels().getData()[index];
        sprintf(buf, "GRAY %3d", valA);
        break;
    case OF_IMAGE_COLOR:
        valR = image.getPixels().getData()[index * 3];
        valG = image.getPixels().getData()[index * 3 + 1];
        valB = image.getPixels().getData()[index * 3 + 2];
        sprintf(buf, "RGB %3d %3d %3d", valR, valG, valB);
        break;
    case OF_IMAGE_COLOR_ALPHA:
        valR = image.getPixels().getData()[index * 4];
        valG = image.getPixels().getData()[index * 4 + 1];
        valB = image.getPixels().getData()[index * 4 + 2];
        valA = image.getPixels().getData()[index * 4 + 3];
        sprintf(buf, "RGBA %3d %3d %3d %3d", valR, valG, valB, valA);
        break;
    }
}

発展課題: 上の例では,ofApp::mousePressed() の中で,getPixels().getData() メソッドの呼び出しを何度も繰り返していて,あまり効率的とは言えません.得られるのは配列ですが,これをどこか変数に代入して,繰り返し使えるようにするとよいでしょう.これを可能にするのがポインタです.これを反映したプログラムは,つぎのようになります.

void ofApp::mousePressed (int x, int y, int button) {
    if (y >= imgH) return;
    int index = y * imgW + x;
    int valR, valG, valB, valA;
    unsigned char *data;
    data = image.getPixels().getData();
    switch (type) {
    case OF_IMAGE_GRAYSCALE:
        valA = data[index];
        sprintf(buf, "GRAY %3d", valA);
        break;
    case OF_IMAGE_COLOR:
        valR = data[index * 3];
        valG = data[index * 3 + 1];
        valB = data[index * 3 + 2];
        sprintf(buf, "RGB %3d %3d %3d", valR, valG, valB);
        break;
    case OF_IMAGE_COLOR_ALPHA:
        valR = data[index * 4];
        valG = data[index * 4 + 1];
        valB = data[index * 4 + 2];
        valA = data[index * 4 + 3];
        sprintf(buf, "RGBA %3d %3d %3d %3d", valR, valG, valB, valA);
        break;
    }
}

getPixels().getData() メソッドは,unsigned char へのポインタを返します.したがって,unsigned char *data と宣言したポインタ変数 data に代入することができます.ポインタ変数 data は,配列のように添字を使ってその要素にアクセスすることができます.ポインタと配列は互換性があると考えて構いません.

【画像データを書き換える】

画像データを読み出すことができるなら,もちろん書き換えることも可能です.たとえば,上の例で,何かキーを押したとき,画像の中央部分の色(ピクセル値)を反転させるなら,つぎのようなプログラムになります.画像データを書き換えたときは,update() メソッドを呼び出すことで,その変更が有効になります.

図
void ofApp::keyPressed (int key) {
    unsigned char *data = image.getPixels().getData();
    for (int x = imgW / 4; x < imgW * 3 / 4; x++) {
        for (int y = imgH / 4; y < imgH * 3 / 4; y++) {
            int index = y * imgW + x;
            switch (type) {
            case OF_IMAGE_GRAYSCALE:
                data[index] = 255 - data[index];
                break;
            case OF_IMAGE_COLOR:
                data[index * 3    ] = 255 - data[index * 3    ];
                data[index * 3 + 1] = 255 - data[index * 3 + 1];
                data[index * 3 + 2] = 255 - data[index * 3 + 2];
                break;
            case OF_IMAGE_COLOR_ALPHA:
                data[index * 4    ] = 255 - data[index * 4    ];
                data[index * 4 + 1] = 255 - data[index * 4 + 1];
                data[index * 4 + 2] = 255 - data[index * 4 + 2];
                //  alpha stays unchanged
                break;
            }
        }
    }
    //  you must call update(), when you have changed the data
    image.update();
}
【画像データを生成する】

ゼロから画像データを作り出すことも可能です.新しく dojoExample10 として以下のプログラムを動かしてみてください.image.allocate(w, h, type) は,横幅 w,縦高 h で画像タイプ type の画像データを新しく生成します.画像データの初期値は不定です.このプログラムでは,画像中央からの距離に応じて sin() 関数を使ったターゲット模様を生成しています.

図
class ofApp : public BaseApp {
private:
    ofImage image;
    int imgW, imgH;
public:
    (... 変更なし ...)
};
void ofApp::setup () {
    imgW = 200;
    imgH = 200;
    image.allocate(imgW, imgH, OF_IMAGE_GRAYSCALE);
    //  setup window
    ofSetWindowShape(imgW, imgH);
    ofSetFrameRate(30);
}
void ofApp::update () {
    //  generate image
    unsigned char *data = image.getPixels().getData();
    float cx = imgW / 2.0, 
          cy = imgH / 2.0;
    for (int y = 0; y < imgH; y++) {
        for (int x = 0; x < imgW; x++) {
            float dx = x - cx, 
                  dy = y - cy;
            float r = sqrt(dx * dx + dy * dy);
            int val = 255.0 * (0.5 + 0.5 * sin(r / 10.0));
            data[y * imgW + x] = val;
        }
    }
    //  you must call update(), when you have changed the data
    image.update();
}
void ofApp::draw(){
    image.draw(0, 0);
}

発展課題: このターゲット模様を動かしてみましょう.内部変数 rad を用意し,update() の中で少しずつ増加(あるいは減少)させます.この rad を sin() に与える値に加えることで,時間とともに位相をずらしていきます.プログラムを動かして,その視覚的効果を確かめてください.

class ofApp : public ofBaseApp {
private:
    ofImage image;
    int imgW, imgH;
    float rad;
public:
    (... 変更なし ...)
};
void ofApp::setup () {
    (... 変更なし ...)
    rad = 0.0;
}
void ofApp::update () {
    //  move forward/backward
    rad += 0.1;
    //  generate image
    unsigned char *data = image.getPixels().getData();
    float cx = imgW / 2.0, 
          cy = imgH / 2.0;
    for (int y = 0; y < imgH; y++) {
        for (int x = 0; x < imgW; x++) {
            float dx = x - cx, 
                  dy = y - cy;
            float r = sqrt(dx * dx + dy * dy);
            int val = 255.0 * (0.5 + 0.5 * sin(r / 10.0 + rad));
            data[y * imgW + x] = val;
        }
    }
    //  you must call update(), when you have changed the data
    image.update();
}
(... 以下変更なし ...)
動画(ofVideoPlayer, ofVideoGrabber)の扱いかた

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

【動画の再生】

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

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

class ofApp : public ofBaseApp {
private:
    ofVideoPlayer video;
public:
    (... 変更なし ...)
};
ofVideoPlayer
void ofApp::setup () {
    //  setup window
    ofSetWindowShape(320, 240);
    ofSetFrameRate(30);
    //  start player
    video.load("keepon.mov");
    video.play();
}
void ofApp::update () {
    video.update();
}
void ofApp::draw () {
    video.draw(0, 0, 320, 240);
}

Windows で動画再生がうまく行かないとき:

Windows 上の openFrameworks では,コーデック(動画ファイルから画像に変換するプログラム部品)が足らない場合があるようです.推奨されている「K-Lite コーデック」をインストールしてください.

少しだけ改造して,マウスボタンを押し下げている間だけポーズするようにします.ポーズするには,setPaused(true) メソッドを呼び出します.false を与えればポーズは解除されます.

class ofApp : public ofBaseApp {
private:
    ofVideoPlayer video;
    bool isStarted;
public:
    (... 変更なし ...)
};
void ofApp::setup () {
    //  setup window
    ofSetWindowShape(320, 240);
    ofSetFrameRate(30);
    //  start player
    video.load("keepon.mov");
    isStarted = false;
}
void ofApp::update () {
    if (isStarted) {
        video.update();
    }
}
void ofApp::draw () {
    if (isStarted) {
        video.draw(0, 0, 320, 240);
    }
}
void ofApp::mousePressed (int x, int y, int button) {
    if (! isStarted) {
        isStarted = true;
        video.play();
    }
    else {
        video.setPaused(false);
    }
}
void ofApp::mouseReleased  (int x, int y, int button) {
    if (isStarted) {
        video.setPaused(true);
    }
}

発展課題: さらに改造して,動画のフレームごとに getPixels().getData() によって画像データを取り出し,画像が尾を引く(エコーのような)効果を与えてみます.つぎのようなプログラムに改変してください.

ofVideoPlayer
class ofApp : public ofBaseApp {
private:
    ofVideoPlayer video;
    bool isStarted;
    ofImage image;
    int width, height;
public:
    (... 変更なし ...)
};
void ofApp::setup () {
    (... 変更なし ...)
    isStarted = false;
    //  get video size
    width = video.getWidth();
    height = video.getHeight();
    //  image buffer
    image.allocate(width, height, OF_IMAGE_COLOR);
    unsigned char *data = image.getPixels().getData();
    //  black it out
    for (int k = 0; k < width * height * 3; k++) 
        data[k] = 0;
    image.update();
}
void ofApp::update () {
    if (isStarted) {
        video.update();
        unsigned char *vdata = video.getPixels().getData(), 
                      *idata = image.getPixels().getData();
        //  image = 0.9 * image + 0.1 * video;
        for (int k = 0; k < width * height * 3; k++) 
            idata[k] = 0.9 * idata[k] + 0.1 * vdata[k];
        image.update();
    }
}
void ofApp::draw () {
    image.draw(0, 0, 320, 240);
}
(... 以下変更なし ...)

video.getPixels().getData() によって現在の動画フレームを画像データとして取り出すことができます.上の例では,image の画像データと video の画像データを「演算」し,image の画像データを更新しています.

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

カメラから動画像を取得するには ofVideoGrabber クラスを使います.基本的には静止画や動画ファイルと同じ使いかたです.内部での画像データは OF_IMAGE_COLOR (RGB) 形式となります.PCに複数のカメラが接続されている場合,setDeviceID() メソッドによって選択することができます.カメラを切り替えるには,いちど close() してから setDeviceID() でカメラを指定し,再び initGrabber() を呼び出してください.

PCにカメラが接続されている人は,新しく dojoExample12 として,以下のプログラムを試してみてください.基本的にカメラから得られた画像も,ビデオ(ファイル)から得られた画像も,また静止画も,扱い方はほぼ同じです.

class ofApp : public ofBaseApp {
private:
    ofVideoGrabber camera;
public:
    (... 変更なし ...)
};
ofVideoPlayer
void ofApp::setup () {
    //  setup window
    ofSetWindowShape(320, 240);
    ofSetFrameRate(30);
    //  setup grabber
    camera.setDeviceID(0);
    camera.initGrabber(320, 240);
}
void ofApp::update() {
    camera.update();
}
void ofApp::draw() {
    camera.draw(0, 0);
}
(... 以下変更なし ...)

発展課題1: カメラ映像が表示されるだけではおもしろくないので,ホラー映画風にピクセル値を反転させてみましょう.

class ofApp : public ofBaseApp {
private:
    ofVideoGrabber camera;
    ofImage image;
public:
    (... 変更なし ...)
};
ofVideoPlayer
void ofApp::setup () {
    //  setup window
    ofSetWindowShape(320, 240);
    ofSetFrameRate(30);
    //  setup grabber
    camera.setDeviceID(0);
    camera.initGrabber(320, 240);
    //  setup image
    image.allocate(320, 240, OF_IMAGE_COLOR);
}
void ofApp::update() {
    camera.update();
    unsigned char *cdata = camera.getPixels().getData(), 
                  *idata = image.getPixels().getData();
    //  image = -camera;
    for (int k = 0; k < 320 * 240 * 3; k++) {
        idata[k] = 255 - cdata[k];
    }
    image.update();
}
void ofApp::draw() {
    image.draw(0, 0);
}

発展課題2: 画像で出力させる以外にも,つぎのような表示のしかたも考えられます.いろいろ試してみてください

class ofApp : public ofBaseApp {
private:
    ofVideoGrabber camera;
    ofImage image;
    int px[24][32], py[24][32];
public:
    ...
};
ofVideoPlayer
void ofApp::setup () {
    //  setup window
    ofSetWindowShape(320, 240);
    ofSetFrameRate(30);
    ofSetBackgroundColor(0, 0, 0);
    //  setup grabber
    camera.setDeviceID(0);
    camera.initGrabber(320, 240);
    //  setup px,py
    for (int y = 0; y < 24; y++) {
        for (int x = 0; x < 32; x++) {
            px[y][x] = x * 10 + 5;
            py[y][x] = y * 10 + 5;
        }
    }
}
void ofApp::update () {
    camera.update();
}
void ofApp::draw () {
    unsigned char *data = camera.getPixels().getData();
    for (int y = 0; y < 24; y++) {
        for (int x = 0; x < 32; x++) {
            int index = (py[y][x] * 320 + px[y][x]) * 3;
            ofSetColor(data[index], data[index + 1], data[index + 2]);
            ofCircle(px[y][x], py[y][x], 4);
        }
    }
}