opencv_contrib レポジトリに dnn という名前のディレクトリがひそかに出来ており、中を覗いてみると cv::dnn モジュールにDeep Learning関連の実装が含まれていたので軽く試してみました。Google Summer of Code (GSoC) 2015で発表され、GitHubにて実装が公開されたという経緯のようです。
OpenCVのDeep Learningモジュールの紹介
It would be cool if OpenCV could load and run deep networks trained with popular DNN packages like Caffe, Theano or Torch. – Ideas Page for OpenCV Google Summer of Code 2015 (GSoC 2015)
* 2015/12/22 追記
12/21にOpenCV3.1がリリースされ、ここで紹介したDeepLearningモジュールが本体に取り込まれたようです。
環境
- CentOS 7.1 / Ubuntu 14.03 (x86_64)
- GCC 4.8.x
- OpenCV 3.0.0 (+ opencv_contrib master branch)
- GeForce GTX 760
有名なディープラーニングフレームワークである Caffe や Torch で作成されたモデルの読み込みも出来るようになっているので、必要であれば Protocol Buffer などの関連ライブラリのインストールも必要です。ここでは両方試します。
Protocol Bufferは比較的新しいものが必要です。少なくともv2.3.0系だとコンパイルエラーが発生しました。CentOS 6.x系だとソースコードをコンパイルして入れるか、RPM Searchから対応するrpmファイルを探してくる必要があります。v2.5.0以上なら問題なさそうです。Ubuntuでも同様です。
また、CaffeやTorch本体のインストールはモデルを利用するだけであれば必要ありません。ただし、テストデータを生成するスクリプトを走らせる際はそれぞれインストールが必要になるので注意してください。
導入
opencv_contribレポジトリのソースを組み込んだビルド方法は公式サイトにも説明がありますが、僕の環境で指定したCMakeの設定を載せておきます。ぜひ参考になれば。GPU環境(+ CUDA Toolkit)が入っていればオプションを明示的に追加しなくても、自動的にGPU環境に対応したMakefileを作ってくれました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
## CMakeはv2.8.8以降必須 $ cmake -version cmake version 2.8.11 ## Protocol Bufferはv2.5.0以降推奨 $ rpm -qa protobuf protobuf-devel protobuf-compiler protobuf-compiler-2.5.0-7.el7.x86_64 protobuf-2.5.0-7.el7.x86_64 protobuf-devel-2.5.0-7.el7.x86_64 $ cmake \ -D CMAKE_C_COMPILER=/usr/bin/gcc \ -D CMAKE_CXX_COMPILER=/usr/bin/g++ \ -D CMAKE_BUILD_TYPE=RELEASE \ -D CMAKE_INSTALL_PREFIX=/usr/local/opencv-3.0.0 \ -D BUILD_opencv_world=OFF \ -D PYTHON2_EXECUTABLE=/usr/bin/python2.7 \ -D PYTHON2_INCLUDE_DIR=/usr/include/python2.7 \ -D PYTHON2_LIBRARY=/usr/lib64/libpython2.7.so \ -D PYTHON2_NUMPY_INCLUDE_DIRS=/usr/lib64/python2.7/site-packages/numpy/core/include \ -D PYTHON2_PACKAGES_PATH=/usr/lib64/python2.7/site-packages \ -D OPENCV_EXTRA_MODULES_PATH=/tmp/opencv_contrib/modules \ -D opencv_dnn_BUILD_TORCH_IMPORTER=ON \ -D WITH_FFMPEG=ON \ -D WITH_WEBP=ON \ -D WITH_GPHOTO2=OFF \ -D WITH_EIGEN=ON \ -D EIGEN_INCLUDE_PATH=/usr/include/eigen3 \ -D WITH_TBB=ON \ -D TBB_INCLUDE_DIRS=/usr/include/tbb \ -D ENABLE_AVX=ON \ -D WITH_OPENMP=ON \ -D WITH_QT=OFF \ -D WITH_GTK=OFF \ -D BUILD_EXAMPLES=OFF \ -D BUILD_TESTS=OFF \ -D BUILD_PERF_TESTS=OFF \ -D INSTALL_TESTS=OFF \ -D INSTALL_C_EXAMPLES=OFF \ -D CMAKE_C_FLAGS="-march=native" \ -D CMAKE_CXX_FLAGS="-march=native" \ -D INSTALL_PYTHON_EXAMPLES=OFF .. $ make -j8 $ sudo make install |
Torchモデルの読み込みをサポートするには、opencv_dnn_BUILD_TORCH_IMPORTER オプションを有効にする必要があるので注意してください(ただしTorchモデルの読み込み機能は2015/12現在は未実装部分が多い、後述)。また、ここではサーバ機にインストールするのでGUI関連のツールキットは入れていません。パフォーマンス向上のために Eigen3(eigen3-devel) / TBB(tbb-devel) を入れておくのはオススメです。
実装
まずは付属のサンプルコードを参考にして、C++インタフェースからCaffe用の学習済みモデルの読み込みと予測(分類)処理を試してみます。
これ入力
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
// opencv_dnn_test.cpp #include <iostream> #include <fstream> #include <opencv2/dnn.hpp> #include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp> using namespace std; int main(int argc, char** argv) { // ImageNet Caffeリファレンスモデル string protoFile = "bvlc_reference_caffenet/deploy.prototxt"; string modelFile = "bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel"; // 画像ファイル string imageFile = (argc > 1) ? argv[1] : "images/cat.jpg"; // Caffeモデルの読み込み cv::Ptr<cv::dnn::Importer> importer; try { importer = cv::dnn::createCaffeImporter(protoFile, modelFile); } catch(const cv::Exception& e) { cerr << e.msg << endl; exit(-1); } cv::dnn::Net net; importer->populateNet(net); importer.release(); // テスト用の入力画像ファイルの読み込み cv::Mat img = cv::imread(imageFile); if(img.empty()) { cerr << "can't read image: " << imageFile << endl; exit(-1); } try { // 入力画像をリサイズ int cropSize = 224; cv::resize(img, img, cv::Size(cropSize, cropSize)); // Caffeで扱うBlob形式に変換 (実体はcv::Matのラッパークラス) const cv::dnn::Blob inputBlob = cv::dnn::Blob(img); // 入力層に画像を入力 net.setBlob(".data", inputBlob); // フォワードパス(順伝播)の計算 net.forward(); // 出力層(Softmax)の出力を取得, ここに予測結果が格納されている const cv::dnn::Blob prob = net.getBlob("prob"); // Blobオブジェクト内部のMatオブジェクトへの参照を取得 // ImageNet 1000クラス毎の確率(32bits浮動小数点値)が格納された1x1000の行列(ベクトル) const cv::Mat probMat = prob.matRefConst(); // 確率(信頼度)の高い順にソートして、上位5つのインデックスを取得 cv::Mat sorted(probMat.rows, probMat.cols, CV_32F); cv::sortIdx(probMat, sorted, CV_SORT_EVERY_ROW|CV_SORT_DESCENDING); cv::Mat topk = sorted(cv::Rect(0, 0, 5, 1)); // カテゴリ名のリストファイル(synset_words.txt)を読み込み // データ例: categoryList[951] = "lemon"; vector<string> categoryList; string category; ifstream fs("synset_words.txt"); if(!fs.is_open()) { cerr << "can't read file" << endl; exit(-1); } while(getline(fs, category)) { if(category.length()) { categoryList.push_back(category.substr(category.find(' ') + 1)); } } fs.close(); // 予測したカテゴリと確率(信頼度)を出力 cv::Mat_<int>::const_iterator it = topk.begin<int>(); while(it != topk.end<int>()) { cout << categoryList[*it] << " : " << probMat.at<float>(*it) * 100 << " %" << endl; ++it; } } catch(const cv::Exception& e) { cerr << e.msg << endl; } return 0; } |
実行結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ g++ opencv_dnn_test.cpp -o opencv_dnn_test `pkg-config --cflags opencv` `pkg-config --libs opencv` $ ./opencv_dnn_test Attempting to upgrade input file specified using deprecated transformation parameters: bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel Successfully upgraded file specified using deprecated data transformation parameters. Note that future Caffe releases will only support transform_param messages for transformation fields. Attempting to upgrade input file specified using deprecated V1LayerParameter: bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel Successfully upgraded file specified using deprecated V1LayerParameter Net Outputs(1): prob Siamese cat, Siamese : 93.9703 % Egyptian cat : 4.23627 % tabby, tabby cat : 0.365742 % lynx, catamount : 0.19613 % hamster : 0.184294 % |
雑種なんだよなぁ。小型犬より体が大きいからシャムよりラグドールに近いんじゃないかと思うのですが、両親を見たことないからわかりません。。
cv::dnnモジュールは高級なインタフェースが提供されているので書きやすいです。あと、OpenCV側でglogの出力をエミュレートしていてなかなか凝ってるなと思いました。チュートリアルやサンプルコードの内容だとこれ以上の詳しい情報が得られないので、cv::dnnモジュールの実装とDoxygenリファレンスを見ながらいろいろ試してみます。
cv::dnn::Blob クラスはネットワークの各レイヤーにおける内部パラメータ、入出力データを扱うための基本となるクラスで、実装上では cv::Mat または cv::UMat の薄いラッパークラスになっています。現在の実装では cv::UMat に未対応の機能も多いのでここでは cv::Mat を使ったコードのみ載せます。
複数の画像を一括で分類する
cv::dnn::Blob オブジェクトには複数の画像データ(バッチと呼ばれる)を格納することができます。最近のOpenCVのインタフェースは入力に cv::Mat ではなく cv::InputArray を渡すメソッドが多いようです。よくわからない人は std::vector<cv::Mat> を作って渡せばOKです。
Blobの形状(次元数等)については cv::dnn::BlobShape 構造体で管理していて、cv::dnn::Blob#shape メソッドで取得できます。operator<< をオーバーロードしているのでストリームにそのまま渡すこともできるので便利です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
cv::dnn::Blob blob = cv::dnn::Blob(image); // imageは cv::InputArray std::cout << "blob shape: " << blob.shape() << std::endl; // 実行結果例 // データ数、チャンネル数、画像幅、画像高さ blob shape: [1, 3, 224, 224] std::vector<cv::Mat> images; // 画像データのリストを準備 // ... std::vector#push_back で5枚追加 cv::dnn::Blob blob = cv::dnn:Blob(images); std::cout << "blob shape: " << blob.shape() << std::endl; // 実行結果例 blob shape: [5, 3, 224, 224] |
ちなみにCaffeのC++インタフェースでも MemoryDataLayer の入力として std::vector<cv::Mat> を渡すことができます。
1 2 3 4 5 6 7 |
// Caffeでは MemoryDataLayer を作って複数の画像データを入力する std::vector<cv::Mat> images; // 複数の画像データを入れる std::vector<int> labels; // imagesに格納した画像に対応する正解ラベルを入れる // caffe::Net#layersメソッドでネットワーク全体を各レイヤーオブジェクトのリストとして取得 const std::vector<boost::shared_ptr<caffe::Layer<float> > >& layers = net->layers(); // データレイヤー(入力層)に cv::Mat のリストを入力として渡す。 boost::dynamic_pointer_cast<caffe::MemoryDataLayer<float> >(layers[0])->AddMatVector(images, labels); |
その際、prototxtファイルのdataレイヤーも MemoryDataLayer に変更しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
layer { name: "data" type: "MemoryData" top: "data" top: "label" transform_param { mirror: false crop_size: 227 mean_file: "mean.binaryproto" } memory_data_param { batch_size: 1 channels: 3 height: 227 width: 227 } } |
caffe::Layer#AddMatVector メソッドに std::vector<cv::Mat> を入力データとして渡します。第二引数には学習時にラベルデータを渡す仕様になっていますが、予測時には適当なデータを入れておけばOK(ただし nullptr は渡せません)。CaffeのC++インタフェースはBoostをよく使っていることもあり、ついtypedefしたくなるほど読みにくくなることがあるんですよね。
話が逸れましたが、OpenCVの使い方に戻ります。複数の画像を入力とした場合のSoftmax(prob)層のBlobは [入力データ数、クラス数] の大きさの二次元データになり、クラス毎の確率(信頼度)が格納されています。確率上位5位までの予測結果を出力する場合も上述のサンプルコードと同様に書くことができます。現在の実装だとCaffeのArgmaxレイヤーが読み込めないみたいなのでソートする処理も別途書く必要がありました。
画像特徴量の抽出 + SVM/ロジスティック回帰での学習
Softmax(prob)層の出力から確率を得るのではなく、中間層の出力を特徴量として抽出したい場合も簡単です。読み込んだモデルのレイヤー定義(レイヤーの名前)を事前に控えておいてください。わかりやすいようにここではCaffeのリファレンスモデル(CaffeNet)を使います。prototxtファイルは以下にあります。
1 2 3 4 |
net.forward(); // cv::dnn::Net#forward メソッドでフォワードパス(順伝播)の計算 cv::dnn::Blob blob = net.getBlob("fc7"); // 全結合層 fc7 (InnerProduct)のBlobを取得 std::cout << "blob shape: " << blob.shape() << std::endl; // blob shape: [1, 4096] (4096次元の特徴量を抽出) const cv::Mat feature = blob.matRefConst(); // 抽出した特徴量を cv::Mat として取得(参照が返る) |
cv::dnn::Net#getBlob メソッドに該当レイヤーの名前を指定してBlobとして取るだけです。注意すべきは、getBlobメソッドはBlobの一時オブジェクトを返すので、大量の画像をバッチ入力した場合はコピーコストが相当大きくなってしまいます。Caffeの caffe::Net#blob_by_name メソッドのようにスマートポインタで包んだBlobを返してくれると助かるのですが。
Caffe C++インタフェースで特徴抽出する場合は以下のように書きます。前述の通りまずは MemoryDataLayer に画像を入力しておきます。
1 2 3 4 |
// caffe::Net#ForwardPrefilled メソッドでフォワードパス(順伝播)の計算 std::vector<caffe::Blob<float>* > results = net->ForwardPrefilled(nullptr); const boost::shared_ptr<caffe::Blob<float> > blob = net->blob_by_name("fc7"); // 全結合層 fc7 のBlobを取得 const float* feature = blob->cpu_data(); // caffe::Blob#cpu_data メソッドで抽出した特徴量を取得 |
floatのポインタで取れるので、後は取り回ししやすいオブジェクトにデータを詰め替えて利用することになるでしょう。CaffeのAPIは戻り値がスマートポインタだったり生ポインタだったり統一されていないので注意が必要です。
cv::dnnモジュールで抽出した特徴量を使ってSVM(cv::ml::SVM)やロジスティック回帰(cv::ml::LogisticRegression)のモデルを学習するサンプルコードをGitHubに上げてありますので参考までに。SVMの学習する部分だけここでも紹介しておきます。
1 2 3 4 5 6 7 |
// feature(cv::Mat): 特徴量, trainLabel(cv::Mat): 正解ラベル cv::Ptr<cv::ml::TrainData> data = cv::ml::TrainData::create(feature, cv::ml::ROW_SAMPLE, trainLabel, false); cv::Ptr<cv::ml::SVM> clf = cv::ml::SVM::create(); clf->setType(cv::ml::SVM::C_SVC); clf->setKernel(cv::ml::SVM::LINEAR); clf->trainAuto(data, 5); // グリッドサーチ + 交差検証(5-fold)で学習 |
OpenCVのSVM実装は昔と比べると使いやすくなっていて、特に cv::ml::SVM#trainAuto メソッドが便利で、内部でグリッドサーチと交差検証までしてくれます。なにより他のライブラリを併用せずに済むのがプログラマにとっては一番のメリットかもしれません。
Torchモデルの読み込み
Torchモデルの読み込み機能も実装に含まれていましたが、まだ対応しているレイヤーの種類が不十分で未実装な処理も多いので使うのは難しそうです。簡単な構造のネットワークなら一応読み込めました。読み込み処理のコードはCaffeのモデル読み込みとほとんど同じです。
1 2 3 4 5 6 7 8 9 10 11 |
-- LeNet5 like network net = nn.Sequential() net:add(nn.SpatialConvolution(3, 6, 5, 5)) net:add(nn.SpatialMaxPooling(2, 2, 2, 2)) net:add(nn.SpatialConvolution(6, 16, 5, 5)) net:add(nn.SpatialMaxPooling(2, 2, 2, 2)) net:add(nn.Reshape(16*5*5)) net:add(nn.Linear(16*5*5, 120)) net:add(nn.Linear(120, 84)) net:add(nn.Linear(84, 10)) --net:add(nn.LogSoftMax()) |
nn.LogSoftMax もまだ対応していないので実際は使えないモデルですが。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
std::string modelFile = "torch_lenet"; // 上記のTorchモデル(バイナリフォーマット) cv::Ptr<cv::dnn::Importer> importer; try { // ASCIIフォーマットのモデルを読み込む場合は第二引数をfalseにする importer = cv::dnn::createTorchImporter(modelFile, true); } catch(const cv::Exception& e) { std::cerr << e.msg << std::endl; } if(!importer) { std::cerr << "can't load network" << std::endl; exit(-1); } cv::dnn::Net net; importer->populateNet(net); importer.release(); |
cv::dnn::createCaffeImporter から cv::dnn::createTorchImporter に変更するだけです。nn.View は nn.Reshape で代替できますが、nn.LogSoftMax が対応していないと使うのは難しいです。
また、モデルの学習については未サポートとのことです。
Functionality of this module is designed only for forward pass computations (i. e. network testing). A network training is in principle not supported.
OpenCVはマルチプラットフォーム対応ライブラリなので、他のフレームワークで作ったモデルをAndroidやiOS等のデバイス上で読み込んで利用できるようにすると便利そうです。
おまけ: データセットモジュール cv::datasets
画像認識関連の検証で便利なデータセットを読み込むためのモジュールも opencv_contrib レポジトリで提供されています。せっかくなのでこのモジュールの紹介もしておきます。
現在の実装で画像分類用途に使えるものは ImageNet/MNIST/PASCAL VOC/SUN の4つのデータセットが用意されています。CIFAR-10あたりも欲しいですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// cv::datasetsモジュールのサンプルコード // MNISTの例 // 事前に元データをダウンロードして、任意のディレクトリに置いておきます string filePath = "../data/"; // クラス名には OR_ (Object Recognition)のプレフィックスが付く, PASCAL VOCなら OR_pascal cv::Ptr<cv::datasets::OR_mnist> dataset = cv::datasets::OR_mnist::create(); dataset->load(filePath); const vector<cv::Ptr<cv::datasets::Object> >& trainData = dataset->getTrain(); const vector<cv::Ptr<cv::datasets::Object> >& testData = dataset->getTest(); cout << "train data size: " << trainData.size() << endl; // train data size: 60000 cout << "test data size: " << testData.size() << endl; // test data size: 10000 // struct OR_mnistObj : public Object // { // char label; // 0..9 // Mat image; // [28][28] // }; // 一つめのサンプル(ラベルと画像データ)を取得 cv::datasets::OR_mnistObj* example = static_cast<cv::datasets::OR_mnistObj*>(trainData[0].get()); cout << "label: " << static_cast<int>(example->label) << endl; // label: 5 cout << example->image.size() << endl; // [28 x 28] cv::imwrite("train0.png", example->image); |
(← cv::Matなのでそのまま保存できる)
MNISTデータセットの読み込み処理の実装ですが、レガシーなC言語での実装になっていてファイルポインタのNULLチェックすらされていないので、もしファイルが存在しない場合は例外やアサートもなく即座にSEGVします、注意。
画像処理領域に限らないですけど、データセットは提供方法やフォーマットが統一されているわけではありませんから、データ入出力の処理を書くのは地味に面倒なんですよね。こちらの使い方のサンプルコードもGitHubに上げてあります。
おわりに
CaffeやTorch単体ではアプリケーション開発は困難ですが、DeepLearningの機能をOpenCVの文脈に持ってくることができれば、テストの書きやすい綺麗なアプリケーションコードが作成しやすくなるかと思います。また、数あるコンピュータビジョンライブラリの中でもOpenCVのコミュニティは特に大きく、サンプルやチュートリアル等の手厚いサポートも期待できるという安心感があるので、今現在の実装に不備が多かったとしてもマルチプラットフォーム対応等も含めて徐々に整備されていくのでしょう。また、opencv_contrib レポジトリ内には他にも魅力的な機能満載のモジュールがたくさんあるので興味があれば試してみることをオススメします。
今年もたくさんのAdvent Calendarが作られてますね。参加者が少なくて閑散としていたり、管理者ががんばって空枠を埋めている風景を見ながらお酒を飲むのが好きです。また来年ですね。
- opencv-samples / dnn – GitHub (今回作成したサンプルコード群)