聞きかじりめも

主にC++やメディア処理技術などに関して気付いたことを書いていきます.ここが俺のメモ帳だ!

FlyCapture2 SDKがVS2015で動かなくなった件

生存報告も兼ねて.

もう半年以上放置していたのは,しゅーろんとかいうクソイベとDの新生活が思っていた以上に忙しかったためです.

PointGray社のFlyCapture2 SDKにバグがあったのでその報告と対策.

何が起こったのか

【以前】 Windows 8.1(64bit)環境で,VisualStudio2013で開発していたときには何も問題なくビルドができていた.

【今回】 同じ環境で,VisualStudio2015にアップグレードしたら以前のコードに大量のエラー. なんかFlyCapture2::Cameraとか重要なやつに赤い波線が…

クラス宣言の外側に無効な型指定子があります.

namespace"FlyCapture2"にメンバー"Camera"がありません.

ぶっちゃけありえな~い!

原因

FlyCapture2::Camera等の型指定子FLYCAPTURE2_APIの定義は FlyCapture2Platform.h内のマクロで決められているが,こいつがVS2015以降に仕事しなくなったのだ.

// 前略
#if defined(WIN32) || defined(WIN64)
// Windows 32-bit and 64-bit
// 中略
#elif defined(MAC_OSX)

// Mac OSX

#else
// Linux and all others
// 後略

元々,VisualStudioの定義済みマクロは_WIN32_WIN64など,_が前に付いているのが普通であり, 今までWIN32とかWIN64が使えていたほうが不思議だったらしい. FlyCapture2 SDKは,この謎のマクロに依存した書き方をしていたため,VS2015以降の仕様変更(?)で動かなくなったのだろう.

参考:

定義済みマクロ

visual studio - C++ MSVC14.0 での WIN32マクロについて - スタック・オーバーフロー

対策

FlyCapture2.hをインクルードする前にWIN32をdefineしてしまえばいい.たとえば次の様に.

#ifdef _WIN32
#define WIN32
#endif
#ifdef _WIN64
#define WIN64
#endif

#include <FlyCapture2.h>
#include <FlyCapture2GUI.h>

以上.

この件については公式サポートに問い合わせメールを出しておきました.

cv::waitKey()の処理時間を計測してみた

ニコ動っぽいタイトルになっちゃったけど嫌いな人は許して(笑)

なんか遅くね?

PointGray社のFlea3は最大120fpsまで出る素晴らしい高速カメラですが,OpenGLを使わずOpenCVだけと連携させたところ,どうしても64fps以上出ないという問題にぶち当たりました.これではこのカメラを使う意味がないのでどうにか80fps以上は出せるようにしたい.そこで処理時間を計測してみたところ,どうやらcv::waitKey()で相当時間をかけているようなので,実際に計測してみました.

計測用コード

画像取得の部分に使ってるのは自作クラスですが,これについては以前の記事を参照のこと.

#include "FlyCap2CVWrapper.h"

using namespace FlyCapture2;

int main(void)
{
    FlyCap2CVWrapper cam;
    int count = 0;
    double time100 = 0;
    int waitparam = 1;
    // capture loop
    char key = 0;
    while (key != 'q')
    {

        // Get the image
        cv::Mat image = cam.readImage();
        flip(image, image, 1);
        cv::imshow("image", image);

        double f = 1000.0 / cv::getTickFrequency();
        int64 time = cv::getTickCount();

        key = cv::waitKey(waitparam);

        if (count == 1000)
        {// TickCountの変化を[ms]単位で表示 1000回平均
            std::cout << "param = " << waitparam << ", time = " << time100 / (count+1) << " [ms]" << std::endl;
            time100 = 0;
            count = 0;
            waitparam++;
        }
        else
        {
            count++;
            time100 += (cv::getTickCount() - time)*f;
        }
    }

    return 0;
}

1000回ループしたときの平均時間を表示したらcv::waitKey()の引数を1ずつ上昇させていくコードです. 風の噂によるとcv::getTickCount()は1msくらいの精度はあるみたいなので使っています.本当かどうか良く分かりませんが...

ちなみに計測環境は,Windows8.1 64bit,Intel Xeon X5675,12GB RAMです.

結果

f:id:Mzawa2:20151228175054j:plain

これはひどい

なんと,cv::waitKey()はせいぜい15ms程度の精度しかないことが分かりました.

ちなみにカメラ入力を描画するまでの処理は1.2msでした.

原因

これはcv::waitKey()の実装を見ればわかります.sources\modules\highgui\src\window_w32.cppの1900行目にあります.

CV_IMPL int
cvWaitKey( int delay )
{
    int time0 = GetTickCount();

    for(;;)
    {
        CvWindow* window;
        MSG message;
        int is_processed = 0;

        if( (delay > 0 && abs((int)(GetTickCount() - time0)) >= delay) || hg_windows == 0 )
            return -1;

        if( delay <= 0 )
            GetMessage(&message, 0, 0, 0);
        else if( PeekMessage(&message, 0, 0, 0, PM_REMOVE) == FALSE )
        {
            Sleep(1);
            continue;
        }

        for( window = hg_windows; window != 0 && is_processed == 0; window = window->next )
        {
            if( window->hwnd == message.hwnd || window->frame == message.hwnd )
            {
                is_processed = 1;
                switch(message.message)
                {
                case WM_DESTROY:
                case WM_CHAR:
                    DispatchMessage(&message);
                    return (int)message.wParam;

                case WM_SYSKEYDOWN:
                    if( message.wParam == VK_F10 )
                    {
                        is_processed = 1;
                        return (int)(message.wParam << 16);
                    }
                    break;

                case WM_KEYDOWN:
                    TranslateMessage(&message);
                    if( (message.wParam >= VK_F1 && message.wParam <= VK_F24)       ||
                        message.wParam == VK_HOME   || message.wParam == VK_END     ||
                        message.wParam == VK_UP     || message.wParam == VK_DOWN    ||
                        message.wParam == VK_LEFT   || message.wParam == VK_RIGHT   ||
                        message.wParam == VK_INSERT || message.wParam == VK_DELETE  ||
                        message.wParam == VK_PRIOR  || message.wParam == VK_NEXT )
                    {
                        DispatchMessage(&message);
                        is_processed = 1;
                        return (int)(message.wParam << 16);
                    }

                    // Intercept Ctrl+C for copy to clipboard
                    if ('C' == message.wParam && (::GetKeyState(VK_CONTROL)>>15))
                        ::SendMessage(message.hwnd, WM_COPY, 0, 0);

                    // Intercept Ctrl+S for "save as" dialog
                    if ('S' == message.wParam && (::GetKeyState(VK_CONTROL)>>15))
                        showSaveDialog(window);

                default:
                    DispatchMessage(&message);
                    is_processed = 1;
                    break;
                }
            }
        }

        if( !is_processed )
        {
            TranslateMessage(&message);
            DispatchMessage(&message);
        }
    }
}

あの精度が低いことで有名なGetTickCount()を内部で使っているんですね.メディア処理プログラムでは御法度レベルなんですがこれ...Intelさんなんで未だにリアルタイム画像処理ライブラリでこんなの使ってんの?

ただし,cv::waitKey()は内部的にOpenGLでいうglutSwapBuffers()に相当する処理も兼ねてるらしいので,HighGUIを使う限りこいつからは逃れられません.もっとFPSを上げたい場合は画像処理だけOpenCVにやらせてあとは別の描画ライブラリを使うようにしましょう.

とはいえこれはwindows環境での話なので,別のOSならまた結果は違うかもしれませんね.Linuxだともっと早いかも.

ARToolKitとOpenCVの歪み補正の違い

本記事ではARToolKitPlusを対象としますが,その本家であるARToolKitでも通用する話ですので,タイトルはARToolKitにしました.

ARToolKitPlusでの歪み補正

ARToolKitPlusのレンズ歪みの式は次の通り.(ARToolKitの公式チュートリアルより引用)

f:id:Mzawa2:20151220221100p:plain f:id:Mzawa2:20151220220658p:plain

このように歪みベクトルとして,歪みの中心座標(x0, y0),歪み係数f,スケールファクターs(センサの物理的大きさの逆数に対応?)を持ちます.基本的に半径方向の歪みのみを考慮しています.

OpenCVでの歪み補正

一方OpenCVでは,レンズ歪みの式を次のように規定しています.(OpenCV2.2 C++リファレンスより引用)

f:id:Mzawa2:20151220221505p:plain

ARToolKitPlusの方とは式が全く異なる上に,OpenCVの方では半径方向だけでなく円周方向の歪みも考慮されています.ちなみに歪み係数ベクトルdist_coeffs[]の並びは(k1, k2, p1, p2, k3(, k4, k5, k6))です.k4以降はデフォルトでは考慮されません.

ここで言いたいのは,「OpenCVdist_coeffs[]ARToolKitdist_factor[]にそのまま入れると破綻する」という事です.OpenCVで得たレンズ歪みパラメータはARToolKit(Plus)上ではそのまま使えないのです.(ただし内部パラメータは使える)

解決策

ではどうするか.簡単です.ARToolKitPlusに渡す前にOpenCV上で歪み補正してやればいいのです.つまり,

  1. OpenCVの内部パラメータをもとに,歪みベクトルが全て0のARToolKitPlus用のパラメータを用意する.
  2. ARToolKitPlus初期化の際に外部ファイルを使用せず,上記を渡す.
  3. ARToolKitPlusの歪み補正を無効化する.
  4. 画像を得る.
  5. OpenCVの機能で歪み補正する.
  6. ARToolKitPlusに渡す

という方針でやります.以下が必要な部分だけ取り出したコードです.

class OpenCVCamera : ARToolKitPlus::Camera
{
public:
    /**
   * Takes the OpenCV camera matrix and distortion coefficients, and generates
   * ARToolKitPlus compatible Camera.
   */
    static Camera * fromOpenCV(const cv::Mat& cameraMatrix, const cv::Mat& distCoeffs, cv::Size size)
    {
        Camera *cam = new ARToolKitPlus::CameraImpl;

        cam->xsize = size.width;
        cam->ysize = size.height;

        for (int i = 0; i < 3; i++)
        for (int j = 0; j < 4; j++)
            cam->mat[i][j] = 0;

        float fx = (float)cameraMatrix.at<double>(0, 0);
        float fy = (float)cameraMatrix.at<double>(1, 1);
        float cx = (float)cameraMatrix.at<double>(0, 2);
        float cy = (float)cameraMatrix.at<double>(1, 2);

        cam->mat[0][0] = fx;
        cam->mat[1][1] = fy;
        cam->mat[0][2] = cx;
        cam->mat[1][2] = cy;
        cam->mat[2][2] = 1.0;

        // OpenCVの歪み補正とARToolKitの歪み補正は全く別の計算式を使っているため,単純に値が使えない
        // ここではARToolKitの歪みベクトルをゼロとし,OpenCV側で補正してやることにする
        for (int i = 0; i < 4; i++) {
            cam->dist_factor[i] = 0;
        }
        return cam;
    }
}

int main()
{
    // ARToolKitPlusの初期化
    ARToolKitPlus::Camera *param = OpenCVCamera::fromOpenCV(cameraMatrix, distCoeffs, cameraSize);

    ARToolKitPlus::Logger *logger = nullptr;
    tracker = new ARToolKitPlus::TrackerSingleMarkerImpl<6, 6, 6, 1, 10>(cameraSize.width, cameraSize.height);
    tracker->init(NULL, 0.1f, 5000.0f);   // ファイルは使用しない
    tracker->setCamera(param);
    tracker->activateAutoThreshold(true);
    tracker->setNumAutoThresholdRetries(5);
    tracker->setBorderWidth(0.125f);            // BCH boader width = 12.5%
    tracker->setPatternWidth(60.0f);            // marker physical width = 60.0mm
    tracker->setPixelFormat(ARToolKitPlus::PIXEL_FORMAT_BGR);        // With OpenCV
    tracker->setUndistortionMode(ARToolKitPlus::UNDIST_NONE);        // UndistortionはOpenCV側で行う
    tracker->setMarkerMode(ARToolKitPlus::MARKER_ID_BCH);
    tracker->setPoseEstimator(ARToolKitPlus::POSE_ESTIMATOR_RPP);

    // Undistort Map
    initUndistortRectifyMap(
        cameraMatrix, distCoeffs,
        Mat(), cameraMatrix, cameraSize, CV_32FC1,
        mapC1, mapC2);

    // メインループ
    while (1)                            
    {
        colorImg = flycap.readImage();     // 適当なカメラ画像読込関数
        Mat temp;
        remap(colorImg, temp, mapC1, mapC2, INTER_LINEAR);
        ARToolKitPlus::ARMarkerInfo *markers;
        int markerID = tracker->calc(temp.data, -1, true, &markers);
        float conf = (float)tracker->getConfidence();      // 信頼度
        if (markerID == 4)
        {
                    // 適当なマーカー位置検出処理
                }
                // 適当な描画処理
    }
}

以上,参考まで.

OpenCVで得たカメラ内部パラメータをOpenGLの射影行列に変換

なぜかネットに正しい情報が少ないのでメモ書き

これまでのあらすじ

ARアプリケーションはARToolKitを使うのが最も楽ですが,残念ながらARToolKitGLUTに大きく依存しており,GLFW+GLEW+GLMを使ってモダンOpenGLで開発している自分にとってこれは非常にありがたくないです.特にGLSLを使う関係上,どうしてもプロジェクション行列・モデルビュー行列を自分で導かなくてはならなくなりました.モデルビュー行列はまだいいとして,プロジェクション行列は今まで適当にしか学んでなかったため,gluPerspective()arglCameraFrustumRH()以外の方法を知らず,OpenCVのカメラキャリブレーションで得た内部パラメータをOpenGLでどう使ったものか途方に暮れていました.arglCameraFrustumRH()の中身もなんかよくわかんないし...

上手くいったコード

//  OpenCVカメラパラメータからOpenGL(GLM)プロジェクション行列を得る関数
void cameraFrustumRH(Mat camMat, Size camSz, glm::mat4 &projMat, double znear, double zfar)
{
    // Load camera parameters
    double fx = camMat.at<double>(0, 0);
    double fy = camMat.at<double>(1, 1);
    double cx = camMat.at<double>(0, 2);
    double cy = camMat.at<double>(1, 2);
    double w = camSz.width, h = camSz.height;

    // 参考:https://strawlab.org/2011/11/05/augmented-reality-with-OpenGL
    // With window_coords=="y_down", we have:
    // [2 * K00 / width,   -2 * K01 / width,   (width - 2 * K02 + 2 * x0) / width,     0]
    // [0,                 2 * K11 / height,   (-height + 2 * K12 + 2 * y0) / height,  0]
    // [0,                 0,                  (-zfar - znear) / (zfar - znear),       -2 * zfar*znear / (zfar - znear)]
    // [0,                 0,                  -1,                                     0]

    
    glm::mat4 projection(
        2.0 * fx / w,      0,                     0,                                     0,
        0,                 2.0 * fy / h,          0,                                     0,
        1.0 - 2.0 * cx / w,   - 1.0 + 2.0 * cy / h, -(zfar + znear) / (zfar - znear),       -1.0,
        0,                 0,                     -2.0 * zfar * znear / (zfar - znear),  0);
    projMat = projection;
}

いろんな実装例を試しましたが,これがうまくいっています(引用元がどこだったか忘れました...).数学的にどうなってるのかまだ良く分かってないのでこれから勉強します.projectionの初期化時に転置されているように見えますが,これはGLMライブラリ(ひいてはOpenGL)の特性によるもので,実際には転置されていません.

(追記:2015/12/04)引用元はMicrosoft RoomAlive Toolkitのソースコードです.元のコードはDirectXなので左手座標系になっていますが,OpenGLの右手座標系にするためにZ軸を全て-1倍したものが上記のソースコードになります.

(修正:2015/12/21) 上手くいってるように思ってましたが,よく見ると間違ってることに気付いたので修正.参考資料は→ https://strawlab.org/2011/11/05/augmented-reality-with-OpenGL

(追記:2016/06/16) 上の透視投影行列の導き方をアップしました.

OpenCVの内部パラメータでOpenGLの透視投影行列を作成(そのに) - 聞きかじりめも

使い方

ちなみに,上のソースは次のように使います.

使用APIOpenCV, OpenGL3.3(with GLSL), GLFW, GLEW, GLM, ARToolKitPlusです.

// プロジェクション行列
    glm::mat4 Projection;
    cameraFrustumRH(cameraMatrix, cameraSize, Projection, 0.1, 5000);
// カメラビュー行列
// 光軸方向がZ軸正方向を向くカメラで,Y軸負方向がカメラの上ベクトルとする
// プロジェクション行列はそうなるように作っている
    glm::mat4 View = glm::mat4(1.0)
        * glm::lookAt(
        glm::vec3(0, 0, 0), // カメラの原点
        glm::vec3(0, 0, 1), // 見ている点
        glm::vec3(0, -1, 0)  // カメラの上方向
        );
// モデル行列
// ARマーカーの位置姿勢は,ARToolKitのarTransMat(),ARToolKitPlusのTrackerImpl::getModelViewMatrix()などで手に入れたやつを使う
    glm::mat4 markerTransMat = glm::make_mat4(tracker->getModelViewMatrix()); // ARマーカー位置姿勢
    glm::mat4 Model = glm::mat4(1.0)
        * markerTransMat; 
// モデルビュー行列,プロジェクション行列
// これをGLSLシェーダ―に転送する
    glm::mat4 MV = View * Model;
    glm::mat4 MVP = Projection * MV;

なお,カメラビュー行列をY軸正方向を上とした場合,つまり

 glm::mat4 View = glm::mat4(1.0)
        * glm::lookAt(
        glm::vec3(0, 0, 0), // カメラの原点
        glm::vec3(0, 0, 1), // 見ている点
        glm::vec3(0, 1, 0)  // カメラの上方向
        );

の場合は,cameraFrustumRH()の中身は次のようになります.

//  OpenCVカメラパラメータからOpenGL(GLM)プロジェクション行列を得る関数
void cameraFrustumRH(Mat camMat, Size camSz, glm::mat4 &projMat, double znear, double zfar)
{
    // Load camera parameters
    double fx = camMat.at<double>(0, 0);
    double fy = camMat.at<double>(1, 1);
    double cx = camMat.at<double>(0, 2);
    double cy = camMat.at<double>(1, 2);
    double w = camSz.width, h = camSz.height;

    // 参考:https://strawlab.org/2011/11/05/augmented-reality-with-OpenGL
    // With window_coords=="y_down", we have:
    // [2 * K00 / width,   -2 * K01 / width,   (width - 2 * K02 + 2 * x0) / width,     0]
    // [0,                 2 * K11 / height,   (-height + 2 * K12 + 2 * y0) / height,  0]
    // [0,                 0,                  (-zfar - znear) / (zfar - znear),       -2 * zfar*znear / (zfar - znear)]
    // [0,                 0,                  -1,                                     0]

    
    glm::mat4 projection(
        -2.0 * fx / w,     0,                     0,                                     0,
        0,                 -2.0 * fy / h,         0,                                     0,
        1.0 - 2.0 * cx / w,   - 1.0 + 2.0 * cy / h, -(zfar + znear) / (zfar - znear),       -1.0,
        0,                 0,                     -2.0 * zfar * znear / (zfar - znear),  0);
    projMat = projection;
}

気が向いたら実行結果のスクショを取りたいと思います.(今すぐ用意できる実行結果は企業秘密的にちょっとまずいファイルなので...)

(2015/12/22追記)この行列を使ってARした結果がこんな感じです.

f:id:Mzawa2:20151222163754p:plain:w120 f:id:Mzawa2:20151222163941p:plain:w120 f:id:Mzawa2:20151222163801p:plain:w120 f:id:Mzawa2:20151222163812p:plain:w120

(2016/06/12追記)この行列の導出過程をアップしました.

OpenCVの内部パラメータでOpenGLの透視投影行列を作成(そのに) - 聞きかじりめも

画像ファイルのリネーム方法(VisualStudio 2013)

今回のTipsはOpenCVは殆ど関係ない(いや,ちょっと関係あるかも)です.

要件定義

OpenCVで画像処理していると,画像処理結果をちょこっとリネームして保存したいことがよくあります. 今回私がやりたいことは,

ソースファイルと同じディレクトリにファイル名の末尾にインデックス(もしくはそれに準ずるもの)を付け足したい

です.具体的には次のようなやつです.

./img/src1/hoge.png
↓
./img/src1/hoge_nega.png

方法

ここではVisual Studio 2012以降に登場したtr2::sys::pathクラスを使用しようと思うので, 先人の知恵を参考にして組んでみます.

#include <opencv2\opencv.hpp>
#include <filesystem>

using namespace std;
using namespace cv;

int main(void)
{
    // 元画像の読み込み
    string filename;
    cout << "空白を含まないファイルパスを入力してください.(例:img/file_name.png)\n"
        << "\nOriginal Image File Path = ";
    cin >> filename;
    tr2::sys::path path(filename);
    cout << "path:" << path << endl;
    Mat original = imread(path.relative_path().string());

        //      適当な画像処理
        Mat dst = ~original;

    // 表示
    imshow("元画像", original);
    imshow("反転", dst);
    waitKey(0);

    // リネームして保存
    imwrite(path.parent_path() + "/" + path.stem() + "_nega" + path.extension(), dst);

    destroyAllWindows();

    return 0;
}

コンソールに入力するファイルパスにspaceが含まれていると途中までしか認識してくれませんので, ちゃんとASCIIコードで入力するか,ファイル名からspaceを削除するかしましょう. そうでないと,例えばファイルパスが./img/src 1/hoge.pngだとpath./img/srcまでしか読み込んでくれません.

(2015/11/26追記)

上記の方法は日本語入力に対応していません.具体的には,tr2::sys::pathクラスでは認識できているものの,cv::imread()に渡すときにpath.relative_path().string()string型に変換する際にマルチバイト文字列が壊れてしまうようです.従って,内部データをそのまま渡すことによってこの現象を回避します.具体的には

 Mat original = imread(path._Mystr);

と変更するだけでマルチバイト文字にも対応します.

OpenCVのチェスボードコーナー検出について

キャリブレーションプログラムをデバッグしてたら気付いたのでメモ.

結論から言うと,最近のOpenCVではcv::findChessboardCorners()を使う際にcv::cornerSubPix()必要ない

内部で何が行なわれているか?

ネットに沢山転がっている典型的なチェスボードを使ったOpenCVキャリブレーションサンプルは,例えば次のようなもの.(OpenCV.jpのOpenCV2.2日本語documentationより引用)

Size patternsize(8,6); // 内部にあるコーナーの個数
Mat gray = ....; // 入力画像
vector<Point2f> corners; // 検出されたコーナーがここに入ります

// CALIB_CB_FAST_CHECK を使うと,画像中にチェスボードコーナーが
// 無かった場合に,時間を大幅に節約できます
bool patternfound = findChessboardCorners(gray, patternsize, corners,
        CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE
        + CALIB_CB_FAST_CHECK);

if(patternfound)
  cornerSubPix(gray, corners, Size(11, 11), Size(-1, -1),
    TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1));

drawChessboardCorners(img, patternsize, Mat(corners), patternfound);

さてこれをデバッグしてみると,findChessboardCorners()を通り抜けた瞬間にcornersにサブピクセル精度の座標値が入っているじゃないか!どうもおかしいと思って元々のcvFindChessboardCorners()関数の定義(src/modules/calib3d/src/calibinit.cpp)を見てみると...

CV_IMPL
int cvFindChessboardCorners( const void* arr, CvSize pattern_size,
                             CvPoint2D32f* out_corners, int* out_corner_count,
                             int flags )
{
//-------------537行目まで中略---------------
    if( found )
    {
        cv::Ptr<CvMat> gray;
        if( CV_MAT_CN(img->type) != 1 )
        {
            gray = cvCreateMat(img->rows, img->cols, CV_8UC1);
            cvCvtColor(img, gray, CV_BGR2GRAY);
        }
        else
        {
            gray = cvCloneMat(img);
        }
        int wsize = 2;
        cvFindCornerSubPix( gray, out_corners, pattern_size.width*pattern_size.height,
            cvSize(wsize, wsize), cvSize(-1,-1), cvTermCriteria(CV_TERMCRIT_EPS+CV_TERMCRIT_ITER, 15, 0.1));
    }
//------------------後略---------------------
}

やっぱり.

思った通り,コーナーを見つけた直後にcvFindCornerSubPix()が入っていました.完全にしてやられた気分.

というわけで,この記事を見つけた良い子の皆はcv::cornerSubPix()などという無駄なコードを書くのを今すぐやめましょう.

PointGray社のカメラを使い倒す

本記事は研究や高貴な趣味などでPointGray社の超小型高機能産業用カメラ(Flea3など)を使う人のためのTipsです.ソースコードだけ欲しい方は最後の方にすべてまとめたラッパークラスが書いてありますので,参考にしてください.

※注意!古いPointGrey FlyCapture2 APIにはバグがあるので,VS2015では以下のライブラリはそのままでは動きません(最新版なら問題ないです).この記事に従って修正してください.

自分がPointGrayカメラを使うに至った理由,あるいはKinectV2をやめた理由

今まではKinectV2のRGBカメラでいいじゃんと思っていたのですが,どうやらこいつはKinectV1とは異なり,自動露出や自動ホワイトバランスがOFFにできないらしいという事に気が付きました. このことは海外では散々議論されてきたようで(日本語で調べてもなかなか出てこなくて困った…)我が圧倒的英語力をもって調べた結果,結局本質的な回避方法はないとのことです. (参照: Set Exposure of Kinect 2 Color Camera

注:これはKinectV2の設計思想に由来するもので,複数のサービスが同時に1つのKinectにアクセスできるようにした結果そうなってしまったようです.(1台のPCで複数のKinectV2接続を公式にサポートしていないのもそのため?)ちなみに今のところ,開発チームは今後KinectV2に自動露出をオフにする機能を取り入れる予定はないらしいです.ふざけんな.

そこで私は,そこそこの解像度を持ちながらコンパクトかつ自動露出等が設定可能な高機能カメラが必要になり,たまたま研究室に余っていたPointGray社のカメラを使うことになりました.従ってKinectV2は暫くお払い箱になります.わざわざ入手困難な時期に何個も買ってもらったのに先生ごめんなさい.

Xbox One Kinect センサー

Xbox One Kinect センサー

Knct Adapter for Win En/Zh/Fr/Ja/Es Amer/Asia Hdwr

Knct Adapter for Win En/Zh/Fr/Ja/Es Amer/Asia Hdwr

自分の環境

FlyCapture SDKは私の大好きなOpenCVとの連携が容易で,ビュープラス様が日本語チュートリアルをお出ししていたのもPointGrayカメラを使う理由の一つです.但しこのOpenCV連携はAPIが両方とも最新版ではないのでちょっと情報が古いです.なので英語のリファレンスやサンプルコードを読む羽目になりました.リファレンスはオンラインではなく,FlyCapture2 SDKの中に入ってるdocumentation.chmが参考になります.

デフォルト機能で使う場合

ここはサンプルそのままですが,最低限必要なところだけピックアップしていきます.

 FlyCapture2::Camera flycam;
        FlyCapture2::Error flycamError;

        // Connect the camera
    flycamError = flycam.Connect(0);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to connect to camera" << std::endl;
        return;
    }

カメラとの接続部分です.まあ難しいことはないですね.

 flycamError = flycam.StartCapture();
    if (flycamError == PGRERROR_ISOCH_BANDWIDTH_EXCEEDED)
    {
        std::cout << "Bandwidth exceeded" << std::endl;
        return;
    }
    else if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to start image capture" << std::endl;
        return;
    }

カメラのキャプチャを開始します.別々のエラーコードを吐いてくれる新設設計.

 FlyCapture2::Image flyImg, bgrImg;
    cv::Mat cvImg;

        // Get the image
    flycamError = flycam.RetrieveBuffer(&flyImg);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "capture error" << std::endl;
        return cvImg;
    }
    // convert to bgr
    flyImg.Convert(FlyCapture2::PIXEL_FORMAT_BGR, &bgrImg);
    // convert to OpenCV Mat
    unsigned int rowBytes = (unsigned int)((double)bgrImg.GetReceivedDataSize() / (double)bgrImg.GetRows());
    cvImg = cv::Mat(bgrImg.GetRows(), bgrImg.GetCols(), CV_8UC3, bgrImg.GetData(), rowBytes);

ここはwhileループの中.カメラからBGRのフォーマットで流れてくるとは限らないので,flyImg.Convert()で変換してからcv::Matにぶち込みます.

 flycamError = flycam.StopCapture();
    if (flycamError != PGRERROR_OK)
    {
        // This may fail when the camera was removed, so don't show 
        // an error message
    }
    flycam.Disconnect();

終了処理です.変数を解放する必要はありませんが,ちゃんとflycamを止めてやりましょう.

とまあこれだけだったら何の苦労もなかったんですが,私が欲しいのは自動露出等まで制御可能なやつです.ということでPointGrayカメラをOpenCVで動作させるラッパークラスを作りました.画像取得部分がめちゃめちゃ短くなります.

使い方

最後に全ソースコードを載せますが,このFlyCap2CVWrapperクラスで最も重要な部分はここです.使うときはここを変えてください.

    // Set Video Property
    // Video Mode: Custom(Format 7)
    // Frame Rate: 120fps
    flycamError = flycam.SetVideoModeAndFrameRate(VIDEOMODE_FORMAT7, FRAMERATE_FORMAT7);
    Format7ImageSettings imgSettings;
    imgSettings.offsetX = 268;
    imgSettings.offsetY = 248;
    imgSettings.width = 640;
    imgSettings.height = 480;
    imgSettings.pixelFormat = PIXEL_FORMAT_422YUV8;
    flycamError = flycam.SetFormat7Configuration(&imgSettings, 100.0f);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to set video mode and frame rate" << std::endl;
        return;
    }
    // Disable Auto changes
    autoFrameRate(false, 85.0f);
    autoWhiteBalance(false, 640, 640);
    autoExposure(false, 1.585f);
    autoSaturation(false, 100.0f);
    autoShutter(false, 7.5f);
    autoGain(false, 0.0f);

最初の行でFORMAT7を指定することでカスタム設定にすることを知らせています.documentationを見るときはFormat7に注意しましょう.

imgSettingsは,画像取得ROI(つまり取得画像の切り出し位置)とピクセルフォーマットを指定します.ピクセルフォーマットはいろいろありますが,白黒画像,YUV,RGB,RAW(Bayer配列?)を指定できます.ビット深度は8bitだったり16bitだったり.

最後のautoXxxx()が自動設定をOn/Offする大事な関数です.ここではかなり暗めな自分の環境で最適な数値を入れているので,実際に使うときはSDK付属のサンプルGUIを弄りながら自分で決めましょう.他の設定が欲しくなったらこの関数の中身を参考にしてみてください.

ソースコード

main.cpp

#include "FlyCap2CVWrapper.h"

using namespace FlyCapture2;

int main(void)
{
    FlyCap2CVWrapper cam;

    // capture loop
    char key = 0;
    while (key != 'q')
    {
        // Get the image
        cv::Mat image = cam.readImage();
        cv::imshow("image", image);
        key = cv::waitKey(1);
    }

    return 0;
}

FlyCap2CVWrapper.h

#pragma once

#ifdef _DEBUG
#define FC2_EXT ".lib"
#define CV_EXT "d.lib"
#else
#define FC2_EXT ".lib"
#define CV_EXT ".lib"
#endif
#pragma comment(lib, "FlyCapture2" FC2_EXT)
#pragma comment(lib, "FlyCapture2GUI" FC2_EXT)
#pragma comment(lib, "opencv_world300" CV_EXT)

#include <opencv2\opencv.hpp>
#include <FlyCapture2.h>
#include <FlyCapture2GUI.h>

class FlyCap2CVWrapper
{
protected:
    FlyCapture2::Camera flycam;
    FlyCapture2::CameraInfo flycamInfo;
    FlyCapture2::Error flycamError;
    FlyCapture2::Image flyImg, bgrImg;
    cv::Mat cvImg;

public:
    FlyCap2CVWrapper();
    ~FlyCap2CVWrapper();
    cv::Mat readImage();
    // Settings
    void autoExposure(bool flag, float absValue);
    void autoWhiteBalance(bool flag, int red, int blue);
    void autoSaturation(bool flag, float absValue);
    void autoShutter(bool flag, float ms);
    void autoGain(bool flag, float dB);
    void autoFrameRate(bool flag, float fps);
    bool checkError();
};

FlyCap2CVWrapper.cpp

#include "FlyCap2CVWrapper.h"

using namespace FlyCapture2;

FlyCap2CVWrapper::FlyCap2CVWrapper()
{
    // Connect the camera
    flycamError = flycam.Connect(0);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to connect to camera" << std::endl;
        return;
    }

    // Get the camera info and print it out
    flycamError = flycam.GetCameraInfo(&flycamInfo);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to get camera info from camera" << std::endl;
        return;
    }
    std::cout << flycamInfo.vendorName << " "
        << flycamInfo.modelName << " "
        << flycamInfo.serialNumber << std::endl;

    // Set Video Property
    // Video Mode: Custom(Format 7)
    // Frame Rate: 120fps
    flycamError = flycam.SetVideoModeAndFrameRate(VIDEOMODE_FORMAT7, FRAMERATE_FORMAT7);
    Format7ImageSettings imgSettings;
    imgSettings.offsetX = 268;
    imgSettings.offsetY = 248;
    imgSettings.width = 640;
    imgSettings.height = 480;
    imgSettings.pixelFormat = PIXEL_FORMAT_422YUV8;
    flycamError = flycam.SetFormat7Configuration(&imgSettings, 100.0f);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to set video mode and frame rate" << std::endl;
        return;
    }
    // Disable Auto changes
    autoFrameRate(false, 85.0f);
    autoWhiteBalance(false, 640, 640);
    autoExposure(false, 1.585f);
    autoSaturation(false, 100.0f);
    autoShutter(false, 7.5f);
    autoGain(false, 0.0f);

    flycamError = flycam.StartCapture();
    if (flycamError == PGRERROR_ISOCH_BANDWIDTH_EXCEEDED)
    {
        std::cout << "Bandwidth exceeded" << std::endl;
        return;
    }
    else if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to start image capture" << std::endl;
        return;
    }
}

FlyCap2CVWrapper::~FlyCap2CVWrapper()
{
    flycamError = flycam.StopCapture();
    if (flycamError != PGRERROR_OK)
    {
        // This may fail when the camera was removed, so don't show 
        // an error message
    }
    flycam.Disconnect();
}

// 自動露出設定
// true -> auto, false -> manual
void FlyCap2CVWrapper::autoExposure(bool flag, float absValue = 1.585f)
{
    Property prop;
    prop.type = AUTO_EXPOSURE;
    prop.onOff = true;
    prop.autoManualMode = flag;
    prop.absControl = true;
    prop.absValue = absValue;
    flycamError = flycam.SetProperty(&prop);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to change Auto Exposure Settings" << std::endl;
    }
    return;
}

// 自動ホワイトバランス設定
void FlyCap2CVWrapper::autoWhiteBalance(bool flag, int red = 640, int blue = 640)
{
    Property prop;
    prop.type = WHITE_BALANCE;
    prop.onOff = true;
    prop.autoManualMode = flag;
    prop.valueA = red;
    prop.valueB = blue;
    flycamError = flycam.SetProperty(&prop);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to change Auto White Balance Settings" << std::endl;
    }
    return;
}

// 自動Satulation設定
void FlyCap2CVWrapper::autoSaturation(bool flag, float percent = 50.0f)
{
    Property prop;
    prop.type = SATURATION;
    prop.onOff = true;
    prop.autoManualMode = flag;
    prop.absControl = true;
    prop.absValue = percent;
    flycamError = flycam.SetProperty(&prop);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to change Auto Satulation Settings" << std::endl;
    }
    return;
}

// 自動シャッター速度設定
void FlyCap2CVWrapper::autoShutter(bool flag, float ms = 7.5f)
{
    Property prop;
    prop.type = SHUTTER;
    prop.autoManualMode = flag;
    prop.absControl = true;
    prop.absValue = ms;
    flycamError = flycam.SetProperty(&prop);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to change Auto Shutter Settings" << std::endl;
    }
    return;
}

// 自動ゲイン設定
void FlyCap2CVWrapper::autoGain(bool flag, float gain = 0.0f)
{
    Property prop;
    prop.type = GAIN;
    prop.autoManualMode = flag;
    prop.absControl = true;
    prop.absValue = gain;
    flycamError = flycam.SetProperty(&prop);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to change Auto Gain Settings" << std::endl;
    }
    return;
}

// フレームレート設定
void FlyCap2CVWrapper::autoFrameRate(bool flag, float fps = 85.0f)
{
    Property prop;
    prop.type = FRAME_RATE;
    prop.autoManualMode = flag;
    prop.absControl = true;
    prop.absValue = fps;
    flycamError = flycam.SetProperty(&prop);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "Failed to change Frame Rate Settings" << std::endl;
    }
    return;
}

// cv::Matへの転送
cv::Mat FlyCap2CVWrapper::readImage()
{
    // Get the image
    flycamError = flycam.RetrieveBuffer(&flyImg);
    if (flycamError != PGRERROR_OK)
    {
        std::cout << "capture error" << std::endl;
        return cvImg;
    }
    // convert to bgr
    flyImg.Convert(FlyCapture2::PIXEL_FORMAT_BGR, &bgrImg);
    // convert to OpenCV Mat
    unsigned int rowBytes = (unsigned int)((double)bgrImg.GetReceivedDataSize() / (double)bgrImg.GetRows());
    cvImg = cv::Mat(bgrImg.GetRows(), bgrImg.GetCols(), CV_8UC3, bgrImg.GetData(), rowBytes);

    return cvImg;
}

bool FlyCap2CVWrapper::checkError()
{
    return flycamError != PGRERROR_OK;
}