openFrameworks & OpenCVでColor Tracking(色追跡)

最近、画像認識でアレコレ作るのが楽しくなってきて、今回は指定された色を追跡して、カメラ上での座標を取得するプログラムを作成してみました。完全にopenFrameworks WikiのColorTrackingのページの写経ですが、コメントをつけています。

大まかな流れ

  1. 追跡したい色を指定する
  2. カメラからの画像(colorImg)を取得する
  3. 画像をHSVに変換して、色相(hueImg)、彩度(satImg)、明度(briImg)をそれぞれ画像化する
  4. 各ピクセルを走査して、追跡したい色と近ければ255, 遠ければ0とした2値画像を作成する
  5. ofxCvContourFinderを利用して、追跡したい色の重心(欲しかった座標)を取得する

コード

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxVectorMath.h"
#include "ofxOpenCv.h"

//色の基本的な情報を持ったクラスを作ります。
class Color {
public:
    float hue, sat, bri;
    ofxVec2f pos;
};


class testApp : public ofBaseApp{

    public:
        //この辺はいつも通り
        void setup();
        void update();
        void draw();

        void keyPressed  (int key);
        void keyReleased(int key);
        void mouseMoved(int x, int y );
        void mouseDragged(int x, int y, int button);
        void mousePressed(int x, int y, int button);
        void mouseReleased(int x, int y, int button);
        void windowResized(int w, int h);


    //カメラの映像を取得するためのオブジェクト
    ofVideoGrabber vidGrabber;

    //カメラの幅と高さ
    int camWidth;
    int camHeight;

    //もともとの映像情報
    ofxCvColorImage colorImg;

    //HSV系に変換した映像情報
    ofxCvColorImage colorImgHSV;

    //HSV系の色相、彩度、明度のマップ
    ofxCvGrayscaleImage hueImg;
    ofxCvGrayscaleImage satImg;
    ofxCvGrayscaleImage briImg;

    //色を追跡して輪郭を出すための映像情報
    ofxCvGrayscaleImage reds;

    //追跡する色です。
    Color one;

    //もとの映像情報のピクセルの彩度と明度が
    //指定した色に近ければ255を代入、遠ければ0を代入
    unsigned char * colorTrackedPixelsRed;

    //二値画像
    ofTexture trackedTextureRed;

    //輪郭を判別してくれるメチャクチャ便利なやつです。
    ofxCvContourFinder finderRed;
};

#endif
#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    //最初は色が原点にあることにします
    one.pos = ofxVec2f(0,0);

    //カメラの大きさを指定
    camWidth = 320;
    camHeight = 240;

    //それぞれの映像情報の大きさを指定してあげます。
    colorImg.allocate(camWidth, camHeight);
    colorImgHSV.allocate(camWidth, camHeight);

    hueImg.allocate(camWidth, camHeight);
    satImg.allocate(camWidth, camHeight);
    briImg.allocate(camWidth, camHeight);

    reds.allocate(camWidth, camHeight);

    //二値画像を作るための配列の大きさを指定
    colorTrackedPixelsRed =new unsigned char [camWidth*camHeight];

    //二値画像の大きさ
    trackedTextureRed.allocate(camWidth, camHeight, GL_LUMINANCE);

    //Grabberの「何か」と大きさ設定
    //setVerboseってなんだろ? Verbose:冗長な、言葉数の多い
    vidGrabber.setVerbose(true);
    vidGrabber.initGrabber(camWidth, camHeight);
}

//--------------------------------------------------------------
void testApp::update(){
    //映像を取得!
    vidGrabber.grabFrame();

    //colorImgの中身をピクセルごとに指定
    colorImg.setFromPixels(vidGrabber.getPixels(), camWidth, camHeight);

    //HSV系に変換
    colorImgHSV = colorImg;
    colorImgHSV.convertRgbToHsv();

    //色相、彩度、明度にマッピング
    colorImgHSV.convertToGrayscalePlanarImages(hueImg, satImg, briImg);

    //ここが何やってんのか分からん。
    hueImg.flagImageChanged();
    satImg.flagImageChanged();
    briImg.flagImageChanged();

    //ピクセルの配列をそれぞれに作成
    unsigned char * huePixels = hueImg.getPixels();
    unsigned char * satPixels = satImg.getPixels();
    unsigned char * briPixels = briImg.getPixels();

    //ピクセルの数
    int nPixels = camWidth*camHeight;

    //ピクセルの色が指定した色と色相と彩度が近ければ、
    //colorTrackedPixelsRedに255を、遠ければ0を代入。
    for (int i=0; i < nPixels; i++) {
        if ( (huePixels[i] >=one.hue-12 && huePixels[i] <= one.hue + 12) &&
             (satPixels[i] >=one.sat-24 && satPixels[i] <=one.sat+200)){
            colorTrackedPixelsRed[i] = 255;
        }else {
            colorTrackedPixelsRed[i]=0;
        }

    }

    //colorTrackedPixelsRedをもとにredsを作成
    //redsは輪郭線を求めるためだけにあるのかな?
    reds.setFromPixels(colorTrackedPixelsRed, camWidth, camHeight);

    //輪郭線を見つける
    finderRed.findContours(reds, 10, nPixels/3, 1, false, true);

    //colorTrackedPixelsRedをもとにtrackedTextureRedを作成
    //これが二値画像になってるっぽい
    trackedTextureRed.loadData(colorTrackedPixelsRed,
                 camWidth, camHeight, GL_LUMINANCE);

    //追跡する色の位置を中心にあわせる
    if (finderRed.blobs.size() > 0) {
        one.pos = ofxVec2f(finderRed.blobs[0].centroid.x,
                finderRed.blobs[0].centroid.y);
    }
}

//--------------------------------------------------------------
void testApp::draw(){
    //背景色を指定
    ofBackground(100, 100, 100);

    ofSetColor(0xffffff);

    //元映像を表示
    vidGrabber.draw(0, 0);

    //HSV系に変換したものを表示
    colorImgHSV.draw(340, 0);

    //二値画像を表示
    trackedTextureRed.draw(20, 300);
    ofDrawBitmapString("red", 20, 280);

    //元映像に輪郭線を表示
    finderRed.draw();

    //二値画像の方に輪郭線表示
    glPushMatrix();
        glTranslatef(20, 300, 0);
        finderRed.draw();
    glPopMatrix();

    //追跡する色の位置を表示
    if (finderRed.blobs.size() > 0) {
        char tempStr1[255];
        sprintf(tempStr1, "x:%fny:%f",
          finderRed.blobs[0].centroid.x,
          finderRed.blobs[0].centroid.y);
        ofDrawBitmapString(tempStr1, 20, 250);
    }
}

//--------------------------------------------------------------
void testApp::mousePressed(int x, int y, int button){
    unsigned char * huePixels = hueImg.getPixels();
    unsigned char * satPixels = satImg.getPixels();
    unsigned char * briPixels = briImg.getPixels();

    //クリックした場所の色を追跡する色に設定。
    x=MIN(x,hueImg.width-1);
    y=MIN(y,hueImg.height-1);

    if (button==0) {
        one.hue = huePixels[x+(y*hueImg.width)];
        one.sat = satPixels[x+(y*satImg.width)];
        one.bri = briPixels[x+(y*briImg.width)];
    }
}

testApp::update()の最後にあるone.posに色を追跡した結果の座標が入っています。

応用例

色ごとにシンセサイザーの音、ドラムパターン、フィルターという役割を持たせて、カメラ上で各色がどこにあるのかを解析し、それに応じて音が出力されます。

まとめ

すこしややこしいですが、重要なのはtestApp::update()だけです。大まかな流れを理解した上で、落ち着いて読めばさほど難しいものではないので、是非挑戦してみてください!