ねこと画像処理。
(アイシャ – 池袋 ねころび)
前回のねこと画像処理 part 1 – 素材集めでは猫画像の集め方について整理しました。今回はその集めた猫画像を使って猫検出用の学習モデル(分類器)を作成したいと思います。それにはいろいろと準備が必要です。
モデル(分類器)の配布についてはこのエントリーの後半で説明します。
アノテーションデータの収集
学習モデルを作る前に猫のどの部分を検出するかを決める必要がありますが、今回は猫の顔(頭)部分の検出を行おうと思います。そのためのアノテーションデータ作成補助ツールを作成したのでそれを使ってひたすらデータを集めます。僕一人の作業だと限界があったのですが、クラウドソーシングによりネット上の顔も知らない有志達の協力のおかげであっという間にデータが集まりました。
アノテーションデータ作成補助ツールの作成にあたっては以下のサイトを参考にさせてもらいました。
このエントリーで公開しているのはOpenCV用のサンプル作成補助を行うツールなのですが、今回開発したツールでは他のコンピュータビジョンライブラリでも利用できるように(例えばccvなど)、アノテーションデータ作成部分と教師データファイル作成部分を分離独立させました。アノテーションデータはいったんRDBMS(SQLite3)に保存し、後で各ライブラリ毎のフォーマットで成形書き出しできるような構成に。UIはタブレットPCでの作業を想定してレスポンシブデザインを採用しました。データは全てDBに保存するので、暇な時間を見つけて少しずつ作業を進めていけるところが便利かなと自画自賛しています。
デモとして今回制作したWebツールを公開しておきます。iPad2横向き利用にUIを最適化していますが、もちろんPCのWebブラウザでも動作します。ドラッグ&ドロップで検出したい部分を矩形切り出しするだけのシンプルなツール。これを使って猫の顔部分をひたすら囲ってもらいました。ツールの使い方は左矢印タップで前の画像、右矢印タップで次の画像に切り替え、アノテーション作成済みの画像は上部のチェックアイコンが緑に点灯し、下部のプログレスバーで進捗状況を確認できます。作業進捗状況は全体で共有なので、複数人で同時に作業している場合はプログレスバーが1ずつ進む/戻ることはないので戸惑わないように。
せっかくブラウザで動くWebツールにしたのだから、クラウドソーシング用途でより便利に使えるようにブラッシュアップしていこうと思います。大規模利用を想定する場合はDB周りを上手く設計しないと破綻しますが。それと、上手くゲーミング要素を入れると単調な作業に刺激が加わって作業効率が上がるかもしれません。なんか面白いアイデアないかな。
学習モデル(分類器)の作成
実際にやってみるとわかりますが、動物の顔を検出するのは人間の顔を検出するより遙かに難しいです。顔の造形/テクスチャ等が品種毎に多種多様かつ複雑なので、直感的にも難しいだろうなと想像できます。データはできるだけ大量に用意しておきたいところですが、今回はポジティブサンプル(正例)、ネガティブサンプル(負例)共に約7,000枚の教師データを集めてモデルを作りました。ポジティブサンプルについては前回のエントリーでも述べたように、僕が都内でひたすら撮影して集めた猫写真を用いました(重要)。
検出アルゴリズムはDPM(Deformable Part Models)を試してみるつもりでしたが、猫の体全体の検出ならともかく頭部分の検出にはそこまで有効ではないと考えられるので、今回はお馴染みのBoosting(アンサンブル学習手法の1つ)でモデルを作ります。最新版のOpenCVを使ってモデルを作る場合、使える特徴量はHaar-like、LBP(Local Binary Pattern)、HoG(Histogram of oriented gradients)の3つ。Haar-like特徴を使った場合は学習にかなりの時間を要しますが、LBP特徴だとそれより数倍速く学習が終わるので、ここではモデル作成と検証を速く繰り返すことができるLBP特徴を使ってモデルを作っていきます。検出精度はHaar-like特徴を使った場合とほぼ遜色ない程度までには高められるので心配ありません。HoG特徴についてはまた別の処理で使う際に紹介しようと思います。ちなみに、先日FacebookがDeepFaceと名付けられた顔認識(顔認証)手法に関する論文を公開していましたが、DeepFaceでも特徴点検出にはLBP特徴を利用しているそうです。
- 顔認証技術: DeepFace と Pyramid CNN
- Y. Taigman, M. Yang, M.’A. Ranzato and L. Wolf. DeepFace: Closing the Gap to Human-Level Performance in Face Verification. CVPR2014.
ディープラーニングについては業務でも検証しながら少しずつ取り入れ始めているのですが、まだまだ勉強不足でわからないことだらけです。
話は戻って、Boostingによるモデルの作り方は公式サイトチュートリアルに沿って進めれば簡単です。
ちなみに、OpenCVには LatentSvmDetector というLatent SVMの実装がありますが、いろいろ調べてみたところまだ使い物になるレベルの実装じゃなさそうでした。学習のためのAPIすらまともに揃っていません。
ポジティブサンプルの準備
こちらは前述のWebツールで作成できるアノテーションデータ。フォーマットは以下の通り。
1 2 3 4 |
## positive.dat ## [画像ファイル名] [対象物体数] [X座標] [Y座標] [幅] [高さ]\n img/img1.jpg 1 140 100 45 45 img/img2.jpg 2 100 200 50 50 50 30 25 25 |
上記のテキストファイルを準備したら、次はOpenCVのツールで解釈できる入力ファイル(バイナリファイル)を作ります。これには opencv_createsamples
というツールを利用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ opencv_createsamples -info positive.dat -vec positive.vec -num 8010 -w 40 -h 40 Info file name: positive.dat Img file name: (NULL) Vec file name: positive.vec BG file name: (NULL) Num: 8010 BG color: 0 BG threshold: 80 Invert: FALSE Max intensity deviation: 40 Max x angle: 1.1 Max y angle: 1.1 Max z angle: 0.5 Show samples: FALSE Width: 40 Height: 40 Create training samples from images collection... Done. Created 8010 samples |
ネガティブサンプルの準備
こちらは検出対象が写っていない画像ファイルをたくさん用意して、テキストファイルに画像ファイル名を以下のように書き出しておくだけです。僕はFlickrのCreative Commonsライセンスで公開されている写真をFlickr APIを使って集めました。
1 2 3 |
## negative.dat img_neg/img1.jpg img_neg/img2.jpg |
学習
前述の通り、アンサンブル学習手法の一つであるBoostingを用いて学習を行い分類器を生成します。OpenCVの実装を使う場合は公式サイトの記載にもあるように、利用するツールは opencv_haartraining
ではなく opencv_traincascade
を使うことが推奨されています。
opencv_traincascade ではデフォルトで Gentle Adaboost (GAB) と呼ばれるアルゴリズムで学習を行います。デフォルトパラメータで学習させる場合、numPos
の値はポジティブサンプル数x0.9程度の値にしておけば失敗は少ないです。opencv_traincascade
実行中に表示される consumed
の値を見ながらパラメータをチューニングしていくと良いです。maxFalseAlarmRate
の値を小さくしたい場合はx0.8くらいまで減らさないとエラーで落ちます。
ここでは20ステージ分しっかり回しておきます。最大誤検出率は厳しめに指定している以外は大体デフォルトパラメータで学習させています。
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 |
$ opencv_traincascade -data ./train/cat/lbp -vec positive.vec -bg negative.dat -numPos 6809 -numNeg 6503 -featureType LBP -maxFalseAlarmRate 0.4 -w 40 -h 40 PARAMETERS: cascadeDirName: ./train/cat/lbp vecFileName: positive.vec bgFileName: negative.dat numPos: 6809 numNeg: 6503 numStages: 20 precalcValBufSize[Mb] : 256 precalcIdxBufSize[Mb] : 256 stageType: BOOST featureType: LBP sampleWidth: 40 sampleHeight: 40 boostType: GAB minHitRate: 0.995 maxFalseAlarmRate: 0.4 weightTrimRate: 0.95 maxDepth: 1 maxWeakCount: 100 ===== TRAINING 0-stage ===== <BEGIN POS count : consumed 6809 : 6809 NEG count : acceptanceRatio 6503 : 1 Precalculation time: 14 ... 以下、各ステージ毎に学習の様子が出力されていく |
無事に学習が完了すると、以下のようなカスケードファイルが生成されます。各ステージ毎のモデルが書き出されますが、実際にプログラムから読む込むファイルは cascade.xml ファイルのみです。
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 |
models/cat/lbp ├── cascade.xml ├── params.xml ├── stage0.xml ├── stage1.xml ├── stage10.xml ├── stage11.xml ├── stage12.xml ├── stage13.xml ├── stage14.xml ├── stage15.xml ├── stage16.xml ├── stage17.xml ├── stage18.xml ├── stage19.xml ├── stage2.xml ├── stage3.xml ├── stage4.xml ├── stage5.xml ├── stage6.xml ├── stage7.xml ├── stage8.xml └── stage9.xml 0 directories, 22 files |
教師データが多いとLBP特徴を使って学習させたとしても時間はかかります。正例/負例共に7,000枚程度だとさくらVPSのエントリープラン程度のスペックで40時間程度。もちろん学習中はCPUを100%近く使いますので、Webサービス等が稼働しているサーバでは実行しないようにしましょう。自宅サーバだとファンが盛大に唸ってうるさいので、AWSを使ってそこそこ高性能なインスタンス上で学習させると効率が良いかと思います。
検証
作ったモデルを使って猫検出をしてみます。テストデータは自分で撮った写真とFlickrのCreative Commonsで公開されている写真を使いました。
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 |
// objdetect.cpp #include <iostream> #include <vector> #include <opencv2/opencv.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/objdetect/objdetect.hpp> int main(int argc, char** argv) { using namespace std; if(argc != 3) { cerr << "./objdetect [image file] [cascade file]" << endl; exit(-1); } cv::Mat src_img = cv::imread(argv[1], 1); if(src_img.empty()) { cerr << "cannot load image" << endl; exit(-1); } cv::Mat dst_img = src_img.clone(); string cascade_file = string(argv[2]); cv::CascadeClassifier cascade; cascade.load(cascade_file); if(cascade.empty()) { cerr << "cannot load cascade file" << endl; exit(-1); } vector<cv::Rect> objects; cascade.detectMultiScale(src_img, objects, 1.1, 3); vector<cv::Rect>::const_iterator iter = objects.begin(); cout << "count: " << objects.size() << endl; while(iter!=objects.end()) { cout << "(x, y, width, height) = (" << iter->x << ", " << iter->y << ", " << iter->width << ", " << iter->height << ")" << endl; cv::rectangle(dst_img, cv::Rect(iter->x, iter->y, iter->width, iter->height), cv::Scalar(0, 0, 255), 2); ++iter; } cv::imwrite("result.jpg", dst_img); return 0; } |
* コンパイル/実行
1 2 3 4 |
$ g++ -o objdetect objdetect.cpp -I/path/to/opencv/include -L/path/to/opencv/lib -lopencv_core -lopencv_highgui -lopencv_objdetect $ ./objdetect cat.jpg cascade.xml count: 1 (x, y, width, height) = (278, 185, 65, 65) |
背景のテクスチャが複雑で一見難しそうな構図ですが、なんとか猫の顔を検出できました。
一度失敗しても cv::CascadeClassifier#detectMultiScale
のパラメータを調整すれば上手く検出できることがあるので吟味します。これがけっこう難しいんですけど。
Pythonバインディングを使う場合は以下のようになります。cv::Mat
が numpy.ndarray
として扱われることに注意すれば後はC++版と大体同じです。APIコールのオーバーヘッドはそれなりにあります。
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 |
#!/usr/local/bin/python # -*- coding: utf-8 -*- import sys import cv2 as cv def detect(imagefilename, cascadefilename): srcimg = cv.imread(imagefilename) if srcimg is None: print('cannot load image') sys.exit(-1) dstimg = srcimg.copy() cascade = cv.CascadeClassifier(cascadefilename) if cascade.empty(): print('cannnot load cascade file') sys.exit(-1) objects = cascade.detectMultiScale(srcimg, 1.1, 3) for (x, y, w, h) in objects: print(x, y, w, h) cv.rectangle(dstimg, (x, y), (x + w, y + h), (0, 0, 255), 2) return dstimg if __name__ == '__main__': result = detect('cat.jpg', 'cascade.xml') cv.imwrite('result.jpg', result) |
長毛種の検出は短毛種よりもかなり難しかったので、ノルウェージャンフォレストキャットを始めとした長毛種の教師データは短毛種の2倍ほど多く準備しました。精度はまだ低いですが麗しのショコラ姫も検出できました。
このくらい小さくても見つけられました。
真正面じゃなくても多少傾いてるくらいなら大丈夫。かわいい。
(My cat! – flicker)
問題点
猫検出に関してはこちらの論文が有名です。
ただ、この論文の手法だとアノテーションデータの収集がさらに困難になってしまうのと、スコティッシュフォールドみたいな耳が立っていない品種では検出精度が低くなると思います。世界中で人気の高い猫を検出できないのは厳しいですよね。機会があればこの論文の詳細な内容紹介もしたいなと思います。
ノイズ/ピンボケ/ブレ対策
ポジティブサンプルには強いデジタルノイズが乗ったものはほとんど入っていないので、例えばスマートフォンで撮影した低画質で高感度ノイズがたくさん乗った写真も教師データに含めておくか、前処理でノイズ除去をがんばるかして対策を講じる必要がありそうです。また、動物は当然動きますのでピンボケや被写体ブレのある写真もデータに含めるかどうかも難しい問題です。
色情報も使う? / 色収差問題
また、猫は品種によって顔の輪郭があいまいになるので(特に長毛種はふさふさなので輪郭付近にはノイズのような細かいエッジが立つ)、勾配情報だけに頼った手法だと精度が上がらないことがわかりました。人検出の分野では、例えば Integral Channel Features (ICF) のように色情報も上手に活用する手法があります。こういう手法を猫にも適用したいところですが、猫の場合は一つの品種でも毛色/柄はたくさん表れます。悩ましい。
しかし、色を扱うとなるとレンズの色収差(フリンジ含む)の影響も少なからず受けるんでしょうか。色収差は画像処理の世界ではエッジと呼ばれる輝度変化が多い部分に表れますので、どれくらいの影響があるのかは検証してみたいなと思います。
高速化/分散処理
今回使ったBoostingアルゴリズムは逐次的に弱識別器を構築するという手法なのでスケールさせるのはそれなりに面倒です。現在の分散機械学習のソリューションがテキストデータを対象にしたものばっかりなのが辛いところですが、画像/音声などのバイナリデータに適用できるようになるのはもうしばらく待つ必要がありそうかなという感じ? 特徴抽出モジュールを作る際にデコーダー周りがテキストを扱うよりも要素技術的に難しいのと、映像データを扱う場合はコーデックの特許周りでMPEG-LAの名前がチラついたりしてどんより嫌な感じになったりするんですね。仕事だと”アカデミック用途に限り無償”とかいう便利技は使えませんので。
品種の偏り
つまるところ、教師データの偏りという機械学習の一般的な問題の話になるのですが、前述したように今回集めたデータは品種が偏ってしまっています。特に都内のねこカフェだと純血種ばかりなので雑種のデータの方が実は集めにくいです。キジトラは例外っぽいけど。ところで、芸能人が雑種の猫飼ってるとなんか好感が持てるのは僕だけですかね。
スコティッシュフォールド特有の可愛さ
折れ耳が特徴的な女性にも人気のかわいい猫。しかし他の品種と比べて検出は難しくなります。猫に限らず動物の耳って特徴的なので貴重な情報源になるんですが、スコティッシュフォールドの場合はその特徴を活用できません。前述の論文のようにペット検出系の論文ではパーツモデルのようなものを使っている例が多いんですが、人間の耳と違って猫の耳の可動域は思いのほか広くてシルエットも変わりやすいので、実際のスナップ写真に適用するのは難しいです。さらにスコティッシュフォールドにはハイランドフォールド(ロングヘアフォールド)と呼ばれる長毛種もいて、こちらはふさふさ折れ耳で検出難度がさらに高くなります。かわいすぎる。
黒猫特有の美しさ
黒猫は画像工学的にも光学的にも処理が難しい品種。特徴が取れません。オートフォーカスもよく外します。さらに飼い猫の場合は毛並みがツヤツヤで光をよく反射するので照明変動の影響も強く受けます。なので黒猫に限ってはシルエットなどの大域的な特徴を使う他ないのかなと思います。長毛だと絶望的な気はしますが。。
こういう風に綺麗にシルエットが出ている写真だとなんとか上手く取れました。
(ファイル:Koteczekdzika.JPG – wikipedia)
他にも問題はたくさんあってキリがないのですが、検出精度自体を上げるのは難しくても、誤検出を減らしていくような処理は比較的簡単に追加していけるので、その方面から改良していくのが懸命かなと思います。
モデル配布
せっかくなので今回作ったモデルを公開しておきます。OpenCVで読み込むことができるカスケードファイル(cascade.xml
)です。
データベースに教師データが新たに追加されたのを検知して学習を走らせるバッチ処理をサーバ上で動かしているので、継続的により精度の高いモデルに自動更新されていくはずです。
検出時のパラメータをゆるめに指定(minNeighbors
パラメータを小さな値にする)しておくと体感的には検出精度は上がるようです。当然誤検出も増えますが、検出領域が一定のサイズ以下の場合は除く(minSize
パラメータを指定する)といった処理を入れればそれなりの精度に見せかけることはできます。
最後に
モデル構築の一番のハードルは「教師データの収集」に尽きるかと思いますが、現在はクラウドソーシングという素晴らしいソリューションがあるので活用するのがおすすめです。Boosting系のアルゴリズムは他の機械学習アルゴリズムと比べて吟味すべきパラメータが少なく、過学習(Overfitting)しにくいと言われてたりしましたが、クラウドソーシングで集めたデータには大きなノイズ(異常値)を含んだものもたくさんあり、生データをそのまま使っても理屈通りの高い汎化能力を発揮するのは難しいです。クリーニングするとしても今回のような画像解析用アノテーションデータのゴミを見つけて取り除くのはコストがかかりますので、上手くバランスを取って少量のサンプリングデータで検証を繰り返しながら精度を上げていくのが良いかなと思います。
すばらしい記事ありがとうございます。
機械学習を調べていてたどり着きました。おもしろいです。
素人考えですが、スコティッシュフォールドのみの検出器+スコティッシュフォールドをあきらめた検出器の、ふたつのクラスタに分けて検出器を作れたら精度向上できるのかもしれないですね。