ARToolKitとOpenCVの歪み補正の違い
本記事ではARToolKitPlusを対象としますが,その本家であるARToolKitでも通用する話ですので,タイトルはARToolKitにしました.
ARToolKitPlusでの歪み補正
ARToolKitPlusのレンズ歪みの式は次の通り.(ARToolKitの公式チュートリアルより引用)
このように歪みベクトルとして,歪みの中心座標(x0, y0),歪み係数f,スケールファクターs(センサの物理的大きさの逆数に対応?)を持ちます.基本的に半径方向の歪みのみを考慮しています.
OpenCVでの歪み補正
一方OpenCVでは,レンズ歪みの式を次のように規定しています.(OpenCV2.2 C++リファレンスより引用)
ARToolKitPlusの方とは式が全く異なる上に,OpenCVの方では半径方向だけでなく円周方向の歪みも考慮されています.ちなみに歪み係数ベクトルdist_coeffs[]
の並びは(k1, k2, p1, p2, k3(, k4, k5, k6))
です.k4以降はデフォルトでは考慮されません.
ここで言いたいのは,「OpenCVのdist_coeffs[]
をARToolKitのdist_factor[]
にそのまま入れると破綻する」という事です.OpenCVで得たレンズ歪みパラメータはARToolKit(Plus)上ではそのまま使えないのです.(ただし内部パラメータは使える)
解決策
ではどうするか.簡単です.ARToolKitPlusに渡す前にOpenCV上で歪み補正してやればいいのです.つまり,
- OpenCVの内部パラメータをもとに,歪みベクトルが全て0のARToolKitPlus用のパラメータを用意する.
- ARToolKitPlus初期化の際に外部ファイルを使用せず,上記を渡す.
- ARToolKitPlusの歪み補正を無効化する.
- 画像を得る.
- OpenCVの機能で歪み補正する.
- 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を使うのが最も楽ですが,残念ながらARToolKitはGLUTに大きく依存しており,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の透視投影行列を作成(そのに) - 聞きかじりめも
使い方
ちなみに,上のソースは次のように使います.
使用APIはOpenCV, 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した結果がこんな感じです.
(2016/06/12追記)この行列の導出過程をアップしました.
画像ファイルのリネーム方法(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は暫くお払い箱になります.わざわざ入手困難な時期に何個も買ってもらったのに先生ごめんなさい.
- 出版社/メーカー: 日本マイクロソフト
- 発売日: 2016/07/14
- メディア: Video Game
- この商品を含むブログを見る
Knct Adapter for Win En/Zh/Fr/Ja/Es Amer/Asia Hdwr
- 出版社/メーカー: Microsoft
- 発売日: 2014/10/22
- メディア: Personal Computers
- この商品を含むブログを見る
自分の環境
- OS: Windows 8.1
- 開発環境: Visual Studio 2013 Express
- 開発言語: C++
- カメラ: PointGray Flea3 USB3.0
- 使用API: PointGray FlyCapture2 SDK, OpenCV3.0
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技術を駆使して訓練なしに観客の光を制御する仕掛けをした.
つまりどういうことだってばよ
こんなのを作った.以上.
材料
外側
- ボタン付きペンライト
ガワだけ使うのでボタンがついてりゃなんでもいいです.なんなら3Dプリンタで作ればもっと自由なデザインになります.私は安さ重視で「キングサンダーIK1」というキンブレの中華コピー品を使用しました(2015/01現在在庫・再入荷予定なし).ボタンスイッチは1個だけですが充分です.
電子パーツ
- 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.ペンライトを分解する
分解してみると,発光部根元の小さいLEDを透明アクリル板とビニール製の筒で拡散させて全体を光らせるという安価な実装でした.この筒にLEDのタワーを入れていきます.案外内部スペースが小さいので,基板実装は工夫が必要です.
2.LEDモジュールをはんだ付けする
まず1個目にピンヘッダをつけた後,LEDモジュールの6個の足のうち外側の4個(GND, VDD)を,先程の1個目の上にモジュールの向きを揃えてタワー状に半田づけします.タワーの高さと間隔はペンライトの大きさから考えましょう.次に,1個目のDOピンから2個目のDIピン,2個目のDOから3個目のDI…と9個目のDIまで繋げます. ここの半田付けはクソ面倒ですが根気よく行きましょう.
3.周辺回路を組み立てる
制御マイコンの2階を作る感じで,電源・スイッチ・LED信号線(根元の方のDI)・GNDをそれぞれ,マイコン電源(LED電源)・マイコンIOピン2箇所・電池の−極に繋げます.抵抗が手元になければ最悪省略してしまっても動きます.頑張って半田付けしましょう.
4.基板をペンライト内に納める
試しに作った制御回路を嵌め込んでみると,狭すぎて入らないことがあります.こういう時は思い切って本体を削ったり穴を開けたりしてみましょう.入るようになるかもしれません.
5.LEDタワーをセットする
先ほど製作したLEDタワーをビニール筒の中に納めます.拡散用アクリル板はいらないのでポイしてください.
6.サンプルプログラムを書き込む
LEDを装着できることを確認したら,まずはサンプルプログラムをネットから拾って動かしてみましょう.開発環境は「Digispark tutorial」で,プログラムは「Adafruit Neopixel library」でググれば出てきます(両方英語).書き込み方などの詳細は日本語で解説している個人ブログを参照してください. ただし,サンプルプログラムはそのままでは動きませんので手を加えます.例えば「buttoncycler」なら,次のようにマクロ定数を変えます.
#define BUTTON_PIN 2 → 1 #define PIXEL_PIN 6 → 0 #define PIXEL_COUNT 16 → 9
うまくいけば色々なパターンで光るLEDタワーが確認できるはずです.
7.プログラムを改造する
ここまで来たらあとは皆さんの思うがままにサンプルを改変しちゃいましょう.参考までに私はµ's仕様にしており,かよちん(Lime)→凛ちゃん(Yellow)→(・8・)(White)→ハノケチュン(Dark Orange)→まきちゃん(Red)→にこにー(Deep Pink)→のんちゃん(Dark Violet)→ンミチュン(Blue)→エリチカ(Cyan)→この順でレインボー,という感じにしました.()内の色名はweb用カラーコードを参考にしています.長押しでオフになり最初に戻ります.レインボーのみ長押し中は光が動きます.ソースコードは後で公開するかも.
終わりに
開発当初に思い描いていた,µ'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::loadPolygonFileOBJ
をpcl::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); } }
結果
ミクさんの神々しい御姿が点群データとして表示できました.ちなみに,CloudViewerは既にマウスドラッグ・キー入力操作が仕込まれていますので,OpenGLのように改めてプログラムする必要はありません.キー入力の一覧はh
でコンソールにずらっと表示されます.q
もしくはe
を押すとviewer.wasStopped()
がtrueを返し終了します.
こんな感じで今後もPCLのTipsを不定期にメモ書きしていく予定.