catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

物体検出器をいろいろと試す

Hololensで使える実用的な物体検出器がないか試行錯誤しています。かたりぃなです。

どうして物体検出器?

まず、現状までに作った機能では「特定の条件下」でのみMTGのカードを検出できています。 手順は次の通りです。

  1. ガウシアンフィルタで細かいノイズを除去
  2. キャニー法による輪郭抽出
  3. Hu不変量モーメントで、実際のカードの輪郭と比較

です。

しかし「特定の条件下」というのが曲者で、検出できたりできなかったりと不安定です。 この不安定さのせいで実験の手間がかかってしまいます。

というわけで、真面目に物体検出に取り組んでみました。

結果

今回試した方法だけでは私がやりたいことに届きませんでした。 原因は物体検出の検出率と実行速度のトレードオフがとれないためです。

  • 精度を上げると実行速度が下がる(リアルタイムに検出できない)
  • 高速な検出手法では検出精度が下がる

というわけです。 リアルタイム性を犠牲にしたくはないので、何か妥当な策がないか調べてみました。

物体検出器の種類

とりあえず色々調べてみたことを整理します。

物体検出の方法はたくさんありますが、今回調べた範囲ではこんな感じでした。

方法は大きく分けて2つあって

  1. 特定物体認識によるもの
  2. セグメンテーションによるもの

です。 今回調べたものはほとんどが2に該当します。 今回調べた範囲で、私の目的達成のために使えそうなものは次の3つでした。

  • R-CNN
  • saliency(顕著度)
  • 適当な特徴量と特徴記述

以下の2つは調べてみたものの、負荷が高すぎるのでリアルタイム処理には向いていないです。

  • k-means法
  • グラフカット

最後に、キーワードだけメモします。使えるかもしれない方法論。

  • selective search
  • object proposal
  • objectness-BING

以下は実験の記録です。

実験環境

  • 入力画像は1280x720,グレースケール
  • Windows-PC上で実行

では順にやってみたことを紹介します。

R-CNN

deep-learningです。deep-learningすごい勢いですね。

まず今回やってみたR-CNNについて。

CNNはconbolutional nural networkの略で、畳み込みニューラルネットワークです。

CNNの前についてる"R"は、文脈によって2つあります。

  • 領域抽出で使うRegion-CNN
  • 系列データで使う Recurent-CNN

今回試したのはRegionのほうです。

みんな大好きchainerを作っているPFNETさんがchainercvというものを出してくれているので、これを使うことにします。

公式のマニュアルどおりにインストールして、デモを起動します。

R-CNN(faster-r-cnn)のデモはリポジトリのexample/faster-rcnnディレクトリに入ってます。

こんな感じで画像ファイルを突っ込んであげるだけで、モデルのダウンロードと識別が行われます。

PS C:\myproject\deep_learning\chainercv\examples\faster_rcnn> python .\demo.py .\pic0.jpg --gpu 0

入力 f:id:Catalina1344:20170530225056j:plain

出力 f:id:Catalina1344:20170602231248p:plain

検出できた。すごい。 でも、chainercvさん、、、それテレビモニタちゃう!

なんか簡単にできすぎたので、複数枚のカードも見つけられるか試してみました。

出力 f:id:Catalina1344:20170602231419p:plain

すごい。

でもそれテレビじゃないよ?

テレビだと認識してしまう原因は単純です。 学習済みモデルは"MTGのカード"なんてニッチなオブジェクトを知らないので。 学習した中ではテレビモニタってこんなのだったのでしょう。

ちなみに識別の実行時間を測定してみると2秒くらいでした。 少々厳しいですね。。。

正確性よりもリアルタイム性が欲しいので、別の方法がないか考えてみました。

適当な特徴量と特徴記述

今回特徴量として試してみたのはFASTとBRISKです。

実行時間とコードはこんな感じです。

  • FAST : 30mSec
  • BRISK : 4mSec
 cv::Mat img = cv::imread("testimg.jpg", 0);

    std::vector<cv::KeyPoint> kp;
    cv::FAST(img, kp, 10); // 30msec
    cv::Mat fast_img;
    cv::drawKeypoints(img, kp, fast_img, 255, cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
    cv::imwrite("fast.jpg", fast_img);

    auto brisk = cv::BRISK::create();
    std::vector<cv::KeyPoint> brisk_kp;
    cv::Mat descriptors;
    brisk->compute(img, brisk_kp, descriptors); // 4msec
    cv::Mat brisk_img;
    cv::drawKeypoints(img, kp, brisk_img, 255, cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
    cv::imwrite("brisk.jpg", brisk_img);

FASTで検出した特徴点 f:id:Catalina1344:20170602231217j:plain

BRISKで検出した特徴点 f:id:Catalina1344:20170602231213j:plain

特徴点はそれっぽく出ているのですが、マッチングがうまくいきませんでした。

なぜかというと、画像の特徴量算出をかけると、イラスト部分の特徴点まで抽出されてしまうためです。

イラスト部分まで特徴抽出されてしまうと、カードのフチ画像だけの特徴点とマッチングを試みたとき、どうにもうまくいきません。 Opencvのmatcherが混乱してしまうから?それとも使い方が間違っているのか。

詳細を別の機会に追ってみたいところです。

まあ、この手法は単一マーカーでのARなら良いかもしれません。

しかし私のやりたいことを実現するには一工夫必要そうです。

欲しい機能は「イラスト以外の部分で特徴抽出とマッチング」です。 というのも、カードゲームのイラストは種類が多すぎるためです。

イデアとしては

  • 物体検出、トラッキング、姿勢推定はリアルタイムに行う
  • 実際のイラストの識別はdeep learning側に分類問題として解かせる

といったところです。 トラッキングと姿勢推定さえリアルタイムに動いていれば、「そこに何を表示するか」は多少の遅延があってもエンターテインメント用のアプリとしては成立するだろうと思っています。

遅延が許されるだろうというのは、HoloLensは、装着者(HoloLens本体)とオブジェクトの空間上の位置関係を覚えられるようになっています。

なので、オブジェクトを一度検出してしまえば、あとはオブジェクトの追跡さえできれば良いんじゃないかなと思っています。

というわけで、少し工夫してイラスト部分をマスクすればいいかもと考えました。

しかしマスク画像を作るためにはカードを正しく検出できていなければなりません。 また鶏が先か卵が先かみたいな不毛な話になってしまうので、この手法は一旦保留にします。

ちなみに、以下の条件下であれば、画像中のカードの場所に特徴点が集中していることは確認できました。

  • 模様のない台紙の上に置いたカード
  • 適当なパラメータでcanny法を適用
  • BRISK特徴量を求める

この集中している特徴点を利用できないかと考え、調べたの結果が以下のようなクラスタリングです。

k-means法

いわゆるデータクラスタリングの手法です。 映像中のカードの周辺に特徴点が集中するため、これをクラスタリングにかけてあげればいけるかもしれないという考えからです。

しかし、これは私の目的を達成する手段にはなりませんでした。 まずk-meansという名前のとおり、このアルゴリズムクラスタ数を与えたうえでクラスタリングを行うものです。

カメラからの入力映像に何枚のカードが含まれているかは事前に知ることはできません。

もちろんゲームのプレイ状況をすべて解析できるならクラスタ数を求められるかもしれませんが、敷居が高いです。 クラスタ数を動的に求める方法もあるみたいですが、この手段だけに拘るのもどうかと思い、いったん保留としました。

グラフカット

PRMLの下巻で、グラフカットを使ったノイズ除去の話があったのを思い出しました。 あれを応用できないかと考えました。

  • OpenCV本体のgrabCut
  • OpenCVのcontribにあるximgprocモジュールのグラフカット

この2つを試してみました。 重すぎて今回の用途では使えなさそうでしたが、後者はカードの領域は分離できています。惜しい。

 // 50 sec
    cv::Rect roi(10, 10, 1260, 700);
    cv::Mat dst, mask, bg, fg;
    cv::grabCut(img, dst, roi, bg, fg, 1, cv::GC_INIT_WITH_RECT);
    compare(dst, cv::GC_PR_FGD, mask, cv::CMP_EQ);
    img.copyTo(dst, mask);
    cv::imwrite("graphcat.jpg", dst);

    // 3 Sec
    cv::Mat segmented;
    auto graph_segmenter = cv::ximgproc::segmentation::createGraphSegmentation();
    graph_segmenter->processImage(img, segmented);
    cv::imwrite("graph_segmentation.jpg", segmented);

grabCut f:id:Catalina1344:20170602231230j:plain

graph_segmenter f:id:Catalina1344:20170602231226j:plain

saliencity

日本語では顕著度、顕著性と呼ばれます。

この手法を例えるならば、「ある画像を見たとき、人はまずどこに着目するの」というお話らしいです。 世の中に出回っているけしからん画像も、この理論を応用しているのかもしれませんね。(空想)

さて、この顕著度のアルゴリズムは高速に動作したので、今のところこれが第一候補になりそうです。

顕著度にもいろいろな尺度があるらしいので、2件ほど試してみました。 まだ理屈がよくわかっていないので、動作結果だけ示します。

spectral_residualのほうは特に軽いですね。検出結果を30FPSでのレンダリングに使うなら、リアルタイムに処理できるかもしれません。

 cv::Mat saliency_map;
    auto grained = cv::saliency::Objectness::Saliency::create("FINE_GRAINED");
    grained->computeSaliency(img, saliency_map);// 2 Sec
    cv::imwrite("grained.jpg", saliency_map);
    auto spectral_residual = cv::saliency::Saliency::create("SPECTRAL_RESIDUAL");
    cv::Mat spectral_residual_map;
    spectral_residual->computeSaliency(img, spectral_residual_map);// 20m Sec
    cv::imwrite("spectral_residual.jpg", spectral_residual_map);

SPECTRAL_RESIDUAL f:id:Catalina1344:20170602231237j:plain

FINE_GRAINED f:id:Catalina1344:20170602231223j:plain

感想と今後

思ったより物体検出手法がたくさんあって驚いています。 R-CNNの世界ではselective-searchが流行っているらしいので、次回はこのあたりを調べていきたいと思います。

あとは、もしかしたらHololensから取得できる深度マップをうまく使えば、何か進展があるかもしれません。 それでは今回はこれにて。