OpenCV3系で導入されたdnnモジュールのサンプルで、GoogLeNetのCaffeモデルの読み込みを行うプログラムのサンプルがあったので、自作画像で画像認識をしつつ、使い方を確認してみようという記事です。
GoogLeNetはGoogle発のDeep Learningの画像分類のニューラルネットワークです。発表されたのは2014年で、今ではもっと新しいネットワークも様々発表されてはいますが、22層という多くの層を用いており、分類精度の高いモデルです。
詳細については論文[1]も出ていますし、他に詳しい記事もたくさんあると思うので、今日は取りあえず使ってみる、実践してみるというところに重きを置きたいと思います。
今回の環境
・OS : Windows10(64bit)
・OpenCV 3.4.1(環境構築済)
OpenCVの環境構築は今回はやりません。OpenCVの環境構築はこちらの記事を参考にしてください。試してないですがOpenCVのバージョンは3.4じゃないと上手くいかないかもしれません。
ソースコード
以下のサンプルコードを参考としました。OpenCVのコマンドラインパーサーがあんまり好きではないので外しています。
外部サイト:OpenCV: Load Caffe framework
また、上記サイトのプログラムでは計算時間測定のため同じ画像を複数回認識させていますが、実際の利用では特に意味がないので、その部分は外しています。
#include <opencv2/dnn.hpp> | |
#include <opencv2/imgproc.hpp> | |
#include <opencv2/highgui.hpp> | |
#include <opencv2/core/utils/trace.hpp> | |
#include <fstream> | |
#include <iostream> | |
#include <cstdlib> | |
using namespace cv; | |
using namespace cv::dnn; | |
using namespace std; | |
static void getMaxClass(const Mat &probBlob, int *classId, double *classProb) | |
{ | |
Mat probMat = probBlob.reshape(1, 1); | |
Point classNumber; | |
minMaxLoc(probMat, NULL, classProb, NULL, &classNumber); | |
*classId = classNumber.x; | |
} | |
static std::vector<String> readClassNames(const char *filename = "synset_words.txt") | |
{ | |
std::vector<String> classNames; | |
std::ifstream fp(filename); | |
if (!fp.is_open()) | |
{ | |
std::cerr << "File with classes labels not found: " << filename << std::endl; | |
exit(-1); | |
} | |
std::string name; | |
while (!fp.eof()) | |
{ | |
std::getline(fp, name); | |
if (name.length()) | |
classNames.push_back(name.substr(name.find(' ') + 1)); | |
} | |
fp.close(); | |
return classNames; | |
} | |
int main(int argc, char **argv) | |
{ | |
CV_TRACE_FUNCTION(); | |
String modelTxt = "bvlc_googlenet.prototxt"; | |
String modelBin = "bvlc_googlenet.caffemodel"; | |
String imageFile = "test1.jpg"; | |
Net net; | |
try { | |
net = dnn::readNetFromCaffe(modelTxt, modelBin); | |
} | |
catch (cv::Exception& e) { | |
std::cerr << "Exception: " << e.what() << std::endl; | |
if (net.empty()) | |
{ | |
exit(-1); | |
} | |
} | |
net.setPreferableTarget(DNN_TARGET_OPENCL); | |
Mat img = imread(imageFile); | |
if (img.empty()) | |
{ | |
std::cerr << "Can't read image from the file: " << imageFile << std::endl; | |
exit(-1); | |
} | |
Mat inputBlob = blobFromImage(img, 1.0f, Size(224, 224), | |
Scalar(104, 117, 123), false); | |
net.setInput(inputBlob, "data"); | |
Mat prob = net.forward("prob"); | |
int classId; | |
double classProb; | |
getMaxClass(prob, &classId, &classProb); | |
vector<String> classNames = readClassNames(); | |
cout << "Best class: #" << classId << " '" << classNames.at(classId) << "'" << endl; | |
cout << "Probability: " << classProb * 100 << "%" << endl; | |
resize(img, img, cv::Size(), 0.2, 0.2); | |
imshow("Image", img); | |
waitKey(0); | |
return 0; | |
} |
上記コードを実行する上で必要なものがいくつかあります。
- 学習済のCaffeのモデルのパラメータ(bvlc_googlenet.caffemodel)
- 学習済のCaffeのモデルのネットワーク設定(bvlc_googlenet.prototxt)
- クラスIDとクラス名を対応付けるファイル(synset_words.txt)
- 認識するテスト画像(.jpgとか.pngとか)
4.に関しては今回のプログラムでは使っていますが、必ずしも必要なものではありません。例えばクラスIDが3となったときに、3は「DOG」みたいな感じでIDと名前を対応付けたいときだけに用意する必要があるものです。
今回試すGoogLeNetについては1.~4.は全て自分で作らずとも入手可能なので難しくありません。
以下のページから全てダウンロード可能なはずなのですが、bvlc_googlenet.prototxtとsynset_words.txtはnot foundとなり取得できませんでした。
外部サイト:OpenCV: Load Caffe framework
bvlc_googlenet.prototxtとsynset_words.txtはOpenCV3.4.1のsourcesフォルダの中にも入っていて、そこからも入手可能ですし、それが無理であればインターネットなどで検索すればどこかからは入手可能なように思います。
OpenCVのパッケージから入手する場合には
opencv341\opencv\sources\samples\data\dnn
のフォルダの中にファイルがあります。
用意してきたファイルを全てVisual Studioのプロジェクトのカレントディレクトリに配置すれば、準備はOKです。
String modelTxt = "bvlc_googlenet.prototxt";
String modelBin = "bvlc_googlenet.caffemodel";
String imageFile = "test1.jpg";
認識させたい画像を上記コードのimageFileにセットして認識させてみましょう。
実験
3枚のオリジナル画像で実験してみました。1000クラスの分類のうち、一番近いクラスを確率付きで表示してくれます。
【Shih-Tzu:58.9323%】
概ね正しい気がしますね。
【Castle:49.2096%】
【seashore:38.5309%】
もちろん1000クラスしかないので上手く認識できないものもありますが、こうやって見るとかなり高い精度で分類ができていることがわかります。
今回の環境
今回はOpenCVからCaffeのGoogLeNetモデルを読み込んで実行してみました。
自分で作ったモデルなども同様に読み込むことができると思います。
敢えてOpenCVでCaffeモデルを読み込みたいという需要がどれだけあるのかはわかりませんが、C++のプログラムに簡単に組み込んで使うとかの用途なら、かなり簡単に実現できるので使いやすいのではないでしょうか。