openFrameworksでOpenCV3を使う

  • 29
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

openFrameworks(以下oF)でOpenCVといえばofxOpenCvやkylemacdonaldさんのofxCvがありますが,細かいデータを取ろうとするとこう,痒い所に手が届かないというか,やはり万全ではないといえます.

そもそもOpenCVはC/C++のライブラリだし,やはり個別にインストールして使えるようにすればオリジナルの関数でしかできないようなこともできます.とくに最新のOpenCV3.xを使いたい場合はその方がスマートじゃないかと思います.

というわけで,oFの開発環境にOpenCV3を導入する方法を述べます.

※ OpenCV3とありますが,別のバージョンでもライブラリ名などが違っても同じ感じでできると思います.

実行環境

  • Mac OSX11.6
  • Xcode 7.3.1
  • openFrameworks 0.9.3
  • OpenCV 3.1(Homebrewでインストール)

インストール

OpenCVをダウンロードしてビルドする

どういう方法でもいいですが,自分の場合はHomebrewでインストールしました.Homebrewが入っていればターミナルでこういう感じです↓

% brew install homebrew/science/opencv3   

詳しいやり方は既に他に説明が多いと思うので省略します.

環境設定(Xcode)

oFの新規プロジェクトを作る

v0.8.0以降からはProjectGeneratorで簡単にできますね.
注意点としては,ofxOpenCvとかofxCvなどのOpenCVラッパー系アドオンを入れちゃうと変に干渉しちゃう恐れがあります.ちゃんと試してはいませんが...

これらのアドオンは確かに使いやすくて便利ですが,せっかくオリジナルのOpenCVを使うのですから,ここは清くラッパーに頼らない方向で行きましょう.

インクルードパスを追加する

XcodeのインクルードパスにインストールしたOpenCV3を追加します.

普通に追加してもいいですが,oFのXcodeプロジェクトにはデフォルトで "Project.xcconfig" というのが付いてますので,ルートまでのパスはここに追加したほうが何かといいです.

Screenshot_2016-08-17_02_01_21.png

↑こういう感じで矢印のところに行を追加します.

Project.xcconfig
//OpenCV3
OPENCV_PATH = /usr/local/Cellar/opencv3/3.1.0_3

ちなみに自分みたいにHomebrewでインストールした場合は上のように/usr/local/Cellar/opencv3の後に続くバージョン毎に分けられたディレクトリがルートパスになります.

"3.1.0_3" のところはもちろん自分がインストールしたもので使いたいバージョンを書いて下さい.

こうすると何が嬉しいかというと,プロジェクト設定の方でこのような書き方ができます.

Screenshot 2016-08-17 02.03.28.png

上の画像ではプロジェクト設定の "Build Settings > Header Search Paths"にOpenCV3のインクルードファイルパスを追加してます.

要するに $(OPENCV_PATH) の部分が先ほど書いたルートパスに置き換えられるというわけです.

たった1行追加するだけなら別にいいんですが,このあと大量のライブラリへのパスを追加するのでこうしたほうがいいというわけです.

ライブラリを追加する

次に,OpenCV3のダイナミックライブラリ(.dylib)を追加します.

下の画像のようにプロジェクト設定の "Build Settings > Other Linker Flags" に追加します.

Screenshot 2016-08-17 02.03.47.png

別に全部追加しなくても必要なやつが分かっていればそれだけでいいと思います.

また,Other Linker Flagsにパスを書く以外に,"Build Phases > Link Binary With Libraries"に追加しても行けますが,後々OpenCVのバージョンを変更したい時に面倒なので,ここに書く方法をお勧めします.

本当にバージョンを変更したいときは先ほどProject.xcconfigに追加したOPENCV_PATHを修正するだけで済みます.

ちなみに別のバージョンのOpenCVでは上記ライブラリの名前などいろいろ違うと思いますので確かめて観て下さい.OpenCV3.xであれば画像の通りでたぶんOKです.

テスト

ofApp.h になんでもいいのでOpenCVの何かをインクルードしてみてください.例えば↓

ofApp.h
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>

実行できれば環境設定はたぶんOKです.

オリジナルのOpenCVをoFで使いやすくする

ofPixelsからcv::Matへの変換

OpenCVで画像を扱う時はcv::Matを使います.一方oFではofPixelsが使われます.
※ofImageもデータの実体はofImageの中にあるofPixelsが持っています.

なので,OpenCVの各関数でいろいろやりたいときは否が応でもcv::Matを使う必要が出てきます.

というわけで,ここでは以下の方法を考えます.

  1. 元画像の読み込みをofImage::load()で行う.
  2. 元画像と同じサイズで任意のチャンネル・深度の出力画像(e.g. ofImage, ofPixels..)を用意する.
  3. それらをcv::Matにデータをコピーせずに変換する.
  4. 変換されたcv::Matを使ってOpenCVの関数でいろいろやる.
  5. そうすると最初に用意した出力画像の中身も更新されるので,そのまま使う

ポイントは3番目の「データをコピーせずに」変換することです.といってもcv::MatもofPixelsも中に画素データへのポインタを持っているので,それを渡すだけです.

実は既にkylemacdonaldさんがofxCvのtoCv()関数で同じことを実現していますので,それを参考に以下のテンプレート関数を作ってみました.

template <typename T>
static cv::Mat toCv(ofPixels_<T>& pix)
{
    int depth;
    switch(pix.getBytesPerChannel())
    {
        case 4: depth = CV_32F;
        case 2: depth = CV_16U;
        case 1: default: depth = CV_8U;
    }
    return cv::Mat(pix.getHeight(), pix.getWidth(), CV_MAKETYPE(depth, pix.getNumChannels()), pix.getData(), 0);
}

これをどこかに書いておけばofPixelsからcv::Matにデータをコピーせずに変換できます.

ちなみに,oFのofPixels以外にも,ofFloatPixels,ofShortPixels(それぞれunsigned char, unsigned short, floatとしてデータを保持)を渡すこともできます.これらはcv::Matでは深度CV_8U, CV_16U, CV_32Fに対応します.

使い方はこうです↓


ofImage srcImg; //!< 元画像
ofImage dstImg; //!< 出力画像

// 元画像の読み込みと出力画像のメモリ確保
srcImg.load("test.png");
dstImg.allocate(srcImg.getWidth(), srcImg.getHeight(), OF_IMAGE_GRAYSCALE); //<--- チャンネル数などは出力画像にあわせる

// それぞれをcv::Matに変換
cv::Mat srcMat = toCv(srcImg);
cv::Mat dstMat = toCv(dstImg);

////////////////////////////
/*
 * OpenCVでをいろいろ処理する(srcMatを使ってdstMatを更新する)
 */
////////////////////////////

// 描画するためにテクスチャを更新(ofImage::update()を呼びます)
dstImg.update();

// 画像描画
srcImg.draw(0, 0);
dstImg.draw(srcImg.getWidth(), 0);


その他のoF<=>OpenCV変換

画像以外にもoFとOpenCVでオブジェクトの変換ができれば便利です.例えばofPolylinevector<cv::Point2f>にしたり,ofVec2fcv::Point2fにしたり,ofRectanglecv::Rectにしたりとか..

ですが,その辺もいくつかはkyleさんのofxCvに既に実装されていますし,とくにこのファイルにある関数をそのままコピーして使うのが手っ取り早そうです.

使いやすそうなものを書いておきます(ほとんどコピペですが)


//----------
// oF型 => OpenCV型への変換
//----------

cv::Point2f toCv(ofVec2f vec)
{
    return cv::Point2f(vec.x, vec.y);
}

cv::Point3f toCv(ofVec3f vec)
{
    return cv::Point3f(vec.x, vec.y, vec.z);
}

cv::Rect toCv(ofRectangle rect)
{
    return cv::Rect(rect.x, rect.y, rect.width, rect.height);
}

cv::Mat toCv(ofMesh& mesh)
{
    vector<ofVec3f>& vertices = mesh.getVertices();
    return cv::Mat(1, vertices.size(), CV_32FC3, &vertices[0]);
}

vector<cv::Point2f> toCv(const ofPolyline& polyline)
{
    // if polyline.getVertices() were const, this could wrap toCv(vec<vec2f>)
    vector<cv::Point2f> contour(polyline.size());
    for(int i = 0; i < polyline.size(); i++) {
        contour[i].x = polyline[i].x;
        contour[i].y = polyline[i].y;
    }
    return contour;
}

vector<cv::Point2f> toCv(const vector<ofVec2f>& points)
{
    vector<cv::Point2f> out(points.size());
    for(int i = 0; i < points.size(); i++) {
        out[i].x = points[i].x;
        out[i].y = points[i].y;
    }
    return out;
}

vector<cv::Point3f> toCv(const vector<ofVec3f>& points)
{
    vector<cv::Point3f> out(points.size());
    for(int i = 0; i < points.size(); i++) {
        out[i].x = points[i].x;
        out[i].y = points[i].y;
        out[i].z = points[i].z;
    }
    return out;
}

cv::Scalar toCv(ofColor color)
{
    return cv::Scalar(color.r, color.g, color.b, color.a);
}



//----------
// OpenCV型 => oF型への変換
//----------

ofVec2f toOf(cv::Point2f point)
{
    return ofVec2f(point.x, point.y);
}

ofVec3f toOf(cv::Point3f point)
{
    return ofVec3f(point.x, point.y, point.z);
}

ofRectangle toOf(cv::Rect rect)
{
    return ofRectangle(rect.x, rect.y, rect.width, rect.height);
}

ただし,これらは先に述べた画像の変換と違って値をコピーして変換するしかないので,そこだけ注意して下さい.

サンプルコード

以下にoF(v0.9.3)のサンプルコードを置いておきます.Xcodeプロジェクトファイル(Version7)も入ってるので環境構築の参考になればと思います.

https://github.com/TatsuyaOGth/OpenCV3_on_oF_example

ソースコード

utils.h

#pragma once

#include "ofMain.h"
#include <opencv2/core.hpp>

/**
 *  Convert ofPixels_<T> to cv::Mat, Pixels dose not copied.
 *  @param pix ofPixels, ofFloatPixels, ofShortPixels.
 *  @return cv::Mat
 */
template <typename T>
static cv::Mat toCv(ofPixels_<T>& pix)
{
    int depth;
    switch(pix.getBytesPerChannel())
    {
        case 4: depth = CV_32F;
        case 2: depth = CV_16U;
        case 1: default: depth = CV_8U;
    }
    return cv::Mat(pix.getHeight(), pix.getWidth(), CV_MAKETYPE(depth, pix.getNumChannels()), pix.getData(), 0);
}


ofApp.h

#pragma once

#include "ofMain.h"
#include "utils.h"

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/photo.hpp>


class ofApp : public ofBaseApp{

    public:
        void setup();
        void draw();
        void keyPressed(int key);

    ofImage mLenaImg; // original image
    ofImage mResImg; // result image
};

ofApp.cpp

#include "ofApp.h"

/**
 *  Test AKAZE feature detections (OpenCV3)
 */
static void featureDetection(ofPixels& src, ofPixels* dst)
{
    cv::Mat srcMat = toCv(src);
    cv::Mat dstMat = toCv(*dst);

    cv::Mat descriptors;
    vector<cv::KeyPoint> keyPoints;

    cv::Ptr<cv::AKAZE> detector = cv::AKAZE::create();

    detector->detectAndCompute(srcMat, cv::noArray(), keyPoints, descriptors);
    cv::drawKeypoints(srcMat, keyPoints, dstMat, cv::Scalar::all(-1), cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
}

/**
 *  Test pencil sketch (OpenCV3)
 */

static void pencilSketch(ofPixels& src, ofPixels* dst)
{
    cv::Mat srcMat = toCv(src);
    cv::Mat dstMat = toCv(*dst);
    cv::Mat dst8UC1;

    cv::pencilSketch(srcMat, dst8UC1, dstMat, 60, 0.07f, 0.02f);
}

/**
 *  Test stylization (OpenCV3)
 */
static void stylization(ofPixels& src, ofPixels* dst)
{
    cv::Mat srcMat = toCv(src);
    cv::Mat dstMat = toCv(*dst);

    cv::stylization(srcMat, dstMat, 60, 0.45f);
}



void ofApp::setup(){
    ofSetWindowShape(512*2, 512);

    mLenaImg.load("Lena.png");
    mResImg.allocate(mLenaImg.getWidth(), mLenaImg.getHeight(), mLenaImg.getImageType());

    featureDetection(mLenaImg.getPixels(), &mResImg.getPixels());
    mResImg.update();
}

void ofApp::draw(){

    mLenaImg.draw(0, 0);
    mResImg.draw(512, 0);

    ofDrawBitmapString("Push the number key (1, 2, 3)", 20, 20);
}

void ofApp::keyPressed(int key){

    switch (key)
    {
        case '1':
            featureDetection(mLenaImg.getPixels(), &mResImg.getPixels());
            mResImg.update();
            break;

        case '2':
            pencilSketch(mLenaImg.getPixels(), &mResImg.getPixels());
            mResImg.update();
            break;

        case '3':
            stylization(mLenaImg.getPixels(), &mResImg.getPixels());
            mResImg.update();
            break;
    }
}

実行結果

(左が元画像,右が出力結果)

Screenshot 2016-08-17 04.06.06.jpg

AKAZE特徴量の抽出

Screenshot 2016-08-17 04.06.13.jpg

cv::pencilSketch(鉛筆画風にした画像の生成)

Screenshot 2016-08-17 04.06.17.jpg

cv::stylization(よくわからないですスミマセン...)

上記試した関数は,いずれもOpenCV3から加えられたものです.

参考