聞きかじりめも

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

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

虹色ペンライトを改造

今年の初めに作ってからというものずっと放置していましたが,ようやく記事にする気になりました. 一応身内には制作過程の資料を公開してたのですが...

組み込みプログラミングの部分はかなり端折っているので,この記事を読めば誰でもすぐに作れるって訳ではないです. 興味ある人は参考程度に見ておいてください.

(20181116追記) 4thライブ仕様の新しいAqoursラブライブレードがこれとほぼ同じ実装らしいです。なんか報われた気がします。 (20211021追記) 諸事情により動画のアップロード先を書き換えました.また,Amazon広告を変えました.

問題提起

近年のライブシーンにおいて,多色LEDペンライトを使用する機会が増えている.これは演者を応援するグッズである以上に,観客が色によって能動的に情報発信するツールとしても使用される.観客自体が訓練し一種の舞台装置となる例[1]もあるが,特定のメンバーを応援するためにメンバーのイメージ色を表示する場合が多い.しかしキングブレードに代表される現在市販の「虹色」ペンライトの殆どは光源が一つであるため,光の色が変化していく時間的な虹色発光は可能であるものの,空間的な虹色発光が原理上不可能である.このため,同時に2人以上のメンバー色を表示し応援することが不可能という問題点があった.筆者は,μ's全員を同時に応援することを目的として,最大9色まで同時発光が可能なペンライト「レイン棒」を新規開発し,これらの問題への解決を試みた.本稿ではその製作過程を報告する.(論文のAbstract風に)

[1] 水樹奈々田村ゆかりのライブ会場が好例.彼らは高度に統率されており,群体として一つの大きな光のパフォーマンスを見せる時がある.最近ではサザンオールスターズが,WiFi技術を駆使して訓練なしに観客の光を制御する仕掛けをした.

つまりどういうことだってばよ

f:id:Mzawa2:20151010105742j:plain:w320

こんなのを作った.以上.

材料

外側

  • ボタン付きペンライト

ガワだけ使うのでボタンがついてりゃなんでもいいです.なんなら3Dプリンタで作ればもっと自由なデザインになります.私は安さ重視で「キングサンダーIK1」というキンブレの中華コピー品を使用しました(2015/01現在在庫・再入荷予定なし).ボタンスイッチは1個だけですが充分です.

f:id:Mzawa2:20151010114157j:plain:w320

電子パーツ

f:id:Mzawa2:20151010115513j:plain

  • LEDモジュール

秋月電子通商AE-WS2812Bを使用しました.モジュール自体は最近流行りのAdafruit NeoPixelです.普通の3色LEDではなく,信号線を直列に繋ぐだけで色制御が可能な特殊モジュールです.

(参考: フルカラーシリアルLED NeoPixelを試してみた - The jonki

DigiSparkという超小型Arduino互換ボードを使用しました.海外からの輸入品ですが,1個895円とお買い得です.初心者にも扱いやすいArduinoベースのマイコンなので,開発環境構築が非常に楽です.これくらいの大きさならペンライト内にすっぽりと収まるでしょう.私は収まりませんでした.

(参考: 橋本商会 » 895円の超小型Ardunoクローン DigiSparkを買った

最近は輸入代行業者でも買えるみたいです.

http://www.elefine.jp/SHOP/96285/103406/list.html

  • その他パーツ

DigiSparkと同じ大きさのユニバーサル基板(DigiSparkと一緒に買える).導線,5kΩくらいの抵抗(あれば),半田,ピンソケット,ピンヘッダ,その他工具.プッシュスイッチは元々ペンライトにあったものを流用.

製作方法

1.ペンライトを分解する

f:id:Mzawa2:20151010153245j:plain:w300

分解してみると,発光部根元の小さいLEDを透明アクリル板とビニール製の筒で拡散させて全体を光らせるという安価な実装でした.この筒にLEDのタワーを入れていきます.案外内部スペースが小さいので,基板実装は工夫が必要です.

2.LEDモジュールをはんだ付けする

f:id:Mzawa2:20151010153456j:plain:h300 f:id:Mzawa2:20151011150645j:plain:h300

まず1個目にピンヘッダをつけた後,LEDモジュールの6個の足のうち外側の4個(GND, VDD)を,先程の1個目の上にモジュールの向きを揃えてタワー状に半田づけします.タワーの高さと間隔はペンライトの大きさから考えましょう.次に,1個目のDOピンから2個目のDIピン,2個目のDOから3個目のDI…と9個目のDIまで繋げます. ここの半田付けはクソ面倒ですが根気よく行きましょう.

3.周辺回路を組み立てる

f:id:Mzawa2:20151010153652p:plain:h300

制御マイコンの2階を作る感じで,電源・スイッチ・LED信号線(根元の方のDI)・GNDをそれぞれ,マイコン電源(LED電源)・マイコンIOピン2箇所・電池の−極に繋げます.抵抗が手元になければ最悪省略してしまっても動きます.頑張って半田付けしましょう.

4.基板をペンライト内に納める

f:id:Mzawa2:20151010153943j:plain:h300 f:id:Mzawa2:20151010154010j:plain:h300

試しに作った制御回路を嵌め込んでみると,狭すぎて入らないことがあります.こういう時は思い切って本体を削ったり穴を開けたりしてみましょう.入るようになるかもしれません.

5.LEDタワーをセットする

f:id:Mzawa2:20151010154458j:plain:h300

先ほど製作したLEDタワーをビニール筒の中に納めます.拡散用アクリル板はいらないのでポイしてください.

6.サンプルプログラムを書き込む

LEDを装着できることを確認したら,まずはサンプルプログラムをネットから拾って動かしてみましょう.開発環境は「Digispark tutorial」で,プログラムは「Adafruit Neopixel library」でググれば出てきます(両方英語).書き込み方などの詳細は日本語で解説している個人ブログを参照してください. ただし,サンプルプログラムはそのままでは動きませんので手を加えます.例えば「buttoncycler」なら,次のようにマクロ定数を変えます.

#define BUTTON_PIN 21
#define PIXEL_PIN 60
#define PIXEL_COUNT 169

うまくいけば色々なパターンで光るLEDタワーが確認できるはずです.

www.youtube.com

7.プログラムを改造する

ここまで来たらあとは皆さんの思うがままにサンプルを改変しちゃいましょう.参考までに私はµ's仕様にしており,かよちん(Lime)→凛ちゃん(Yellow)→(・8・)(White)→ハノケチュン(Dark Orange)→まきちゃん(Red)→にこにー(Deep Pink)→のんちゃん(Dark Violet)→ンミチュン(Blue)→エリチカ(Cyan)→この順でレインボー,という感じにしました.()内の色名はweb用カラーコードを参考にしています.長押しでオフになり最初に戻ります.レインボーのみ長押し中は光が動きます.ソースコードは後で公開するかも.

www.youtube.com

終わりに

開発当初に思い描いていた,µ's9人を同時に応援できるペンライトを実現できて大変満足です.今後のオフライブも捗ると思います.今後の発展として,レイン棒を降った時に丁度光のメッセージが読めるような点滅パターンを仕込むなどすると更に表現の幅が広がるので,挑戦したいところです.

しかし残念なことに,多くのライブ(µ'sのライブも含む)では改造・自作ペンライトの使用が禁止されています.自作し終えてから気づいた私は今,ショックで軽く落ち込んでいます.レイン棒が公式に陽の目を見ることがあるのか非常に疑問ですが……

まぁ楽しいからいっか!

(追記:2017/02/04)要望があったのでソースコードを公開します.7.で作成したプログラムは以下の通りですが,その後ちょっと改変したので動画そのものの挙動ではありません.具体的には「終わりに」で述べた「レイン棒を降った時に丁度光のメッセージが読めるような点滅パターン」の実装を試みている部分があります(電源を入れた直後にピンクの点滅が起こりますが,暗い場所でいい感じに横に振るとNICOの文字が見えるかも?).あと,元のbuttoncyclerで使われていた関数は勉強のためそのまま残してあります.

// This is a demonstration on how to use an input device to trigger changes on your neo pixels.
// You should wire a momentary push button to connect from ground to a digital IO pin.  When you
// press the button it will change to a new pixel animation.  Note that you need to press the
// button once to start the first animation!

#include <Adafruit_NeoPixel.h>

#define BUTTON_PIN   1    // Digital IO pin connected to the button. 

#define PIXEL_PIN    0    // Digital IO pin connected to the NeoPixels.

#define PIXEL_COUNT  9    // Number of NeoPixels

#define SHOWTYPE_NUM  11   // Number of color patterns

//--------------------------------
//    μ's Color Code (R, G, B)
//--------------------------------
#define COLOR_NICO     0xff, 0x14, 0x93    //  Deep Pink
#define COLOR_MAKI     0xff, 0x00, 0x00    //  Red
#define COLOR_ELI      0x00, 0xff, 0xff    //  Cyan
#define COLOR_KOTORI   0xff, 0xff, 0xff    //  White Smoke
#define COLOR_HONOKA   0xff, 0x8c, 0x00    //  Dark Orange
#define COLOR_HANAYO   0x00, 0xff, 0x00    //  Lime
#define COLOR_RIN      0xff, 0xff, 0x00    //  Yellow
#define COLOR_UMI      0x00, 0x00, 0xff    //  Blue
#define COLOR_NOZOMI   0x94, 0x00, 0xd3    //  Dark Violet
#define COLOR_BLACK    0x00, 0x00, 0x00

const uint8_t colorCode[9][3] = {
                    {COLOR_HANAYO},
                    {COLOR_RIN},
                    {COLOR_KOTORI},
                    {COLOR_HONOKA},
                    {COLOR_MAKI},
                    {COLOR_NICO},
                    {COLOR_NOZOMI},
                    {COLOR_UMI},
                    {COLOR_ELI}};

const char str[4][5][9] = {
                    {{1,1,1,1,1,1,1,1,1},
                     {0,0,0,0,0,1,1,0,0},
                     {0,0,0,1,1,0,0,0,0},
                     {0,1,1,0,0,0,0,0,0},
                     {1,1,1,1,1,1,1,1,1}},      //  N
                    {{0,0,0,0,0,0,0,0,0},
                     {1,0,0,0,0,0,0,0,1},
                     {1,1,1,1,1,1,1,1,1},
                     {1,0,0,0,0,0,0,0,1},
                     {0,0,0,0,0,0,0,0,0}},      //  I
                    {{0,0,1,1,1,1,1,0,0},
                     {0,1,0,0,0,0,0,1,0},
                     {1,0,0,0,0,0,0,0,1},
                     {0,1,0,0,0,0,0,1,0},
                     {0,0,0,0,0,0,0,0,0}},      //  C
                    {{0,0,1,1,1,1,1,0,0},
                     {0,1,0,0,0,0,0,1,0},
                     {1,0,0,0,0,0,0,0,1},
                     {0,1,0,0,0,0,0,1,0},
                     {0,0,1,1,1,1,1,0,0}}};      //  O

// Parameter 1 = number of pixels in strip,  neopixel stick has 8
// Parameter 2 = pin number (most are valid)
// Parameter 3 = pixel type flags, add together as needed:
//   NEO_RGB     Pixels are wired for RGB bitstream
//   NEO_GRB     Pixels are wired for GRB bitstream, correct for neopixel stick
//   NEO_KHZ400  400 KHz bitstream (e.g. FLORA pixels)
//   NEO_KHZ800  800 KHz bitstream (e.g. High Density LED strip), correct for neopixel stick
Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);

bool oldState = HIGH;
int showType = 0;              // The Number of Color Patterns
unsigned long timeStamp = 0;

void setup() {
  pinMode(BUTTON_PIN, INPUT);
  strip.begin();
  colorWipe(strip.Color(COLOR_KOTORI), 30);
  delay(50);
  colorWipe(strip.Color(0, 0, 0), 30);
  delay(20);
  strip.show(); // Initialize all pixels to 'off'
}

void loop() {
  // Get current button state.
  bool newState = digitalRead(BUTTON_PIN);
  // Check if state changed from high to low (button press).
  if (newState == HIGH && oldState == LOW) {
    // Short delay to debounce button.
    delay(20);
    // Check if button is still low after debounce.
    newState = digitalRead(BUTTON_PIN);
    if (newState == HIGH) {
      showType++;
      if (showType > SHOWTYPE_NUM)
        showType = 0;
      startShow(showType);
      //  Set time stamp on pushing the button.
      timeStamp = millis();
    }
  }
  //  You can reset color pattern by pushing the button for 1 sec.
  else if(newState == HIGH && oldState == HIGH) {
    if((millis() - timeStamp) >= 1000) {
      showType = 0;
      startShow(showType);
    }
  }
  // Set the last button state to the old state.
  oldState = newState;
}

void startShow(int i) {
  switch(i){
    case 0: colorWipe(strip.Color(COLOR_BLACK), 40);    // Black/off
            break;
    case 1: colorCharactor(strip.Color(COLOR_NICO), str, 12);
            break;
    case 2: colorWipe(strip.Color(COLOR_HANAYO), 40);
            break;
    case 3: colorWipe(strip.Color(COLOR_RIN), 40);
            break;
    case 4: colorWipe(strip.Color(COLOR_KOTORI), 40);
            break;
    case 5: colorWipe(strip.Color(COLOR_HONOKA), 40);
            break;
    case 6: colorWipe(strip.Color(COLOR_MAKI), 40);
            break;
    case 7: colorWipe(strip.Color(COLOR_NICO), 40);
            break;
    case 8: colorWipe(strip.Color(COLOR_NOZOMI), 40);
            break;
    case 9: colorWipe(strip.Color(COLOR_UMI), 40);
            break;
    case 10: colorWipe(strip.Color(COLOR_ELI), 40);
            break;
    case 11: rainbowCycleForLoveLive(80);
            break;
  }
}

// Fill the dots one after the other with a color
void colorWipe(uint32_t c, uint8_t wait) {
  for(uint16_t i=0; i<strip.numPixels(); i++) {
      strip.setPixelColor(i, c);
      strip.show();
      delay(wait);
  }
}

void rainbowForLoveLive(uint8_t wait) {
  for(int i=0; i<9; i++) {
      strip.setPixelColor(i, strip.Color(colorCode[i][0],colorCode[i][1],colorCode[i][2]));
      strip.show();
      delay(wait);
  }
}

void colorCharactor(uint32_t c, const char str[][5][9], uint8_t wait) {
    do{
      for(int k=0; k<4; k++) {      //  num of charactors
        for(int j=0; j<5; j++) {    //  width of charactor
          for(int i=0; i<9; i++) {  //  height of charactor
            if(str[k][j][i])
              strip.setPixelColor(i, c);
            else
              strip.setPixelColor(i, strip.Color(COLOR_BLACK));
          }
          strip.show();
          delay(wait);
        }
      }
      for(int i=0; i<9; i++) {  //  height of charactor
        strip.setPixelColor(i, strip.Color(COLOR_BLACK));
      }
      strip.show();
      delay(wait*10);
  }while(digitalRead(BUTTON_PIN) == LOW);
}

void rainbowCycleForLoveLive(uint8_t wait){
  static int8_t j = 0;
  while(digitalRead(BUTTON_PIN) == HIGH) {
    for(int i=0; i<strip.numPixels(); i++) {
      strip.setPixelColor(i, strip.Color(colorCode[j][0], colorCode[j][1], colorCode[j][2]));
      j--;
      if(j<0) j=8;
    }
    strip.show();
    delay(wait);
    j++;
    if(j>8) j=0;
  }
}

void rainbow(uint8_t wait) {
  uint16_t i, j;

  for(j=0; j<256; j++) {
    for(i=0; i<strip.numPixels(); i++) {
      strip.setPixelColor(i, Wheel((i+j) & 255));
    }
    strip.show();
    delay(wait);
  }
}

// Slightly different, this makes the rainbow equally distributed throughout
void rainbowCycle(uint8_t wait) {
  uint16_t i, j;

  for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
    for(i=0; i< strip.numPixels(); i++) {
      strip.setPixelColor(i, Wheel(((i * 256 / strip.numPixels()) + j) & 255));
    }
    strip.show();
    delay(wait);
  }
}

//Theatre-style crawling lights.
void theaterChase(uint32_t c, uint8_t wait) {
  for (int j=0; j<10; j++) {  //do 10 cycles of chasing
    for (int q=0; q < 3; q++) {
      for (int i=0; i < strip.numPixels(); i=i+3) {
        strip.setPixelColor(i+q, c);    //turn every third pixel on
      }
      strip.show();
     
      delay(wait);
     
      for (int i=0; i < strip.numPixels(); i=i+3) {
        strip.setPixelColor(i+q, 0);        //turn every third pixel off
      }
    }
  }
}

//Theatre-style crawling lights with rainbow effect
void theaterChaseRainbow(uint8_t wait) {
  for (int j=0; j < 256; j++) {     // cycle all 256 colors in the wheel
    for (int q=0; q < 3; q++) {
        for (int i=0; i < strip.numPixels(); i=i+3) {
          strip.setPixelColor(i+q, Wheel( (i+j) % 255));    //turn every third pixel on
        }
        strip.show();
       
        delay(wait);
       
        for (int i=0; i < strip.numPixels(); i=i+3) {
          strip.setPixelColor(i+q, 0);        //turn every third pixel off
        }
    }
  }
}

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;
  if(WheelPos < 85) {
   return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  } else if(WheelPos < 170) {
    WheelPos -= 85;
   return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  } else {
   WheelPos -= 170;
   return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
  }
}

(2017/06/09追記) 要望があったので基板の写真を追加します。基本的に回路を銅線で繋いであるだけです。抵抗も特に入れていません。

PCLでOBJファイルを表示(点群のみ)

PCLのチュートリアルでは点群データのファイル拡張子として.pcdを使っていますが,.pcdでない既存のファイルからも点群データを読み込んで表示したいという需要は大いにあると思います.例えば.objとか.stlとか.成功したコードを2通り載せておきます.他にも見つかったら追記しようと思います.

なお,ここでは.objファイルとしてMMD標準モデルとして有名な「あにまさ式ミク」を利用します.元データは.pmdですが,.objファイルにしてくれた先人がいたので利用させてもらいます.感謝感激雨霰.

第一の方法:PolygonMesh→PointCloud

.objファイルはPCLで読み込むとpcl::PolygonMesh型となります.これは直接pcl::Visualization::CloudViewerで開くことができないので,pcl::PointCloud<T>型に変換してからCloudViewerに渡します.なおソースコード中のPCLAdapter.hについては以前の記事を参照のこと.

ちなみにpcl::io::loadPolygonFileOBJpcl::io::loadPolygonFileSTLに変更すれば.stlファイルも読み込めます.

#include "PCLAdapter.h"

const char filename[] = "model/miku.obj";

int main(void)
{
    // OBJファイルを読み込む
    pcl::PolygonMesh::Ptr mesh(new pcl::PolygonMesh());
    pcl::PointCloud<pcl::PointXYZ>::Ptr obj_pcd(new pcl::PointCloud<pcl::PointXYZ>());
    if (pcl::io::loadPolygonFileOBJ(filename, *mesh) != -1)
    {   // PolygonMesh -> PointCloud<PointXYZ>
        pcl::fromPCLPointCloud2(mesh->cloud, *obj_pcd);
    }
    while (!viewer.wasStopped())
    {
        viewer.showCloud(obj_pcd);
    }
}

第二の方法:PolygonMesh→vtkPolyData→PointCloud

なぜわざわざvtkPolyDataとかいう余計なものを間に挟むのか.まあいいじゃないか....っていう冗談はさておき.CloudViewerで物足りなくなった時に将来的にPCLVisualizerを使うことになると思います.こっちはvtkPolyDataを要求するので,この変換方法もメモとして残すためです.どうせGLFWに描かせるようにするからいらないかもしれないけど.

(2015/09/21訂正)間違い.PCLVisualizerはちゃんとPointCloudをそのまま読み込んでくれます.なのでこっちの方法はいよいよもって意味がなくなりました.

#include "PCLAdapter.h"

const char filename[] = "model/miku.obj";

int main(void)
{
    // OBJファイルを読み込む
    pcl::PolygonMesh::Ptr mesh(new pcl::PolygonMesh());
    pcl::PointCloud<pcl::PointXYZ>::Ptr obj_pcd(new pcl::PointCloud<pcl::PointXYZ>());
    if (pcl::io::loadPolygonFileOBJ(filename, *mesh) != -1)
    {   // PolygonMesh -> vtkPolyData -> PointCloud<PointXYZ>
        vtkSmartPointer<vtkPolyData> vtkmesh;
        pcl::io::mesh2vtk(*mesh, vtkmesh);
        pcl::io::vtkPolyDataToPointCloud(vtkmesh, *obj_pcd);
    }
    while (!viewer.wasStopped())
    {
        viewer.showCloud(obj_pcd);
    }
}

結果

f:id:Mzawa2:20150908230037p:plain ミクさんの神々しい御姿が点群データとして表示できました.ちなみに,CloudViewerは既にマウスドラッグ・キー入力操作が仕込まれていますので,OpenGLのように改めてプログラムする必要はありません.キー入力の一覧はhでコンソールにずらっと表示されます.qもしくはeを押すとviewer.wasStopped()がtrueを返し終了します.

こんな感じで今後もPCLのTipsを不定期にメモ書きしていく予定.