Tech Sketch Bucket of Technical Chips by TIS Inc.

OpenCVによる画像認識アプリケーション with Google Glass(2)

Pocket

前回はOpenCVを利用したGlassでのカメラプレビューの表示に関して説明しました。第二回はORBアルゴリズムによる画像認識の実装について記します。ORB(Oriented FAST and Rotated BRIEF)はパテントフリーで高速な特徴量記述のアルゴリズムとして知られています。FAST(Features from Accelerated Segment Test)とBRIEF(Binary Robust Independent Elementary Features) というコーナー検出と特徴量記述のアルゴリズムを基礎に開発されたものです。

画像認識の基礎

実装に入る前に画像認識について説明したいと思います。画像処理では「特徴」という言葉は特別な意味を持ちます。特徴とは、画像データの中の特徴的な部分(コーナー、物体の境界線、矩形の領域、動画のフレーム間の差異など)のことで、アプリケーションに応じて処理しやすいものを特徴として扱います。画像処理の一分野であり、今回題材として扱う画像認識の場合、数学的ないくつかの手順(=アルゴリズム)を踏んで、画像データの中でその画像がどのようなものであるかをよく表している部分を見つけ(=特徴検出)、その特徴的な部分がどのようなものであるかを数値として表す(=特徴抽出)というのが基本的な処理の流れです。

これだけではイメージが掴みにくいと思いますので、実際のアルゴリズムを処理の流れを図を交えながらもう少し噛み砕いて説明したいと思います。ちなみに、ここからの説明はOpenCV公式サイトを元にしています。Pythonのチュートリアルとしてまとめられていますが、画像処理についてよくまとまった説明がされていて、Webリンクも充実しているので、本格的に画像処理を勉強したい方はここから始めれば基礎を深く学べると思います。

特徴とは

ジグソーパズルを作る場合を思い浮かべてください。ジグソーパズルでは、一つ一つのピースを適切な場所に配置することで一枚の絵を完成させますが、このとき人間の脳では様々なことを考えます。最初にパズルの端や角に当たるピースを見つけたり、完成図のイメージと一致するピースを見つけて位置を推測したり、完成しているピースに隣り合うものを探したり、色合いから位置を判別したり、あるいは適当に置いてみたり、、、

このように人間はいろいろな視点を持ってジグソーパズルを完成させようとします。一方コンピュータはこのような画像を解釈する作業が苦手で計算量の多さから処理時間もかかりがちです。しかしながら最近は画像処理の分野も進歩していて様々なアルゴリズムが開発され、効率化が図られています。そのアルゴリズムの中で特徴検出、特徴抽出、そして特徴量マッチングの処理が行われます。

以下の画像で代表的な特徴であるコーナーとエッジを見てみましょう。上部にA〜Fの小さな画像がありますが、これは下の風景画像から部分的に切り取られたものです。A〜Fのうち、どこから切り取られたものであるかを正確に言い当てることができるのはどれでしょうか。EとFですね。この2つの部分は建物の角(コーナー)に当たる部分なのですぐに分かります。次にCとDはどうでしょうか。これらは建物の構造からおおよその位置は判別できますが、正確に言い当てることは難しいと思います(Cは雲の位置で分かりますが)。AとBに関しては画面全体に変化がないため、どの位置に該当するのか分かりません。

ocv_withglass_feature_building

この状況を抽象化して表現すると、次のようになります。大きな画像の一部分を窓越しに覗き、窓を「少しだけずらしてみる」という状況を想定します。すると窓を動かした場合、下記の3パターンが考えられます。これらはコーナー、エッジ、フラットエリアと呼ばれ、このうち窓を動かしたときに変化するもの(コーナーとエッジ)が特徴として検出する対象になります。

  • どの方向に動かしても変化するもの(下図、赤枠)→ コーナー
  • 方向に依存して変化する場合としない場合がある(下図、黒枠)→ エッジ
  • どの方向に動かしても変化しない(下図、青枠)→ フラットエリア

ocv_withglass_feature_simple

このように少しずらすことで窓越しに見た景色が変化するような場所を計算により探すことが、画像認識における第一歩である特徴検出(ここではコーナー検出)になります。次に見つけた特徴の周りにどのようなものがあるかを数値化して表すことが特徴抽出です。特徴抽出の計算結果により得られるものは多次元のベクトルであることが多いです。最終的には、2つの画像の特徴抽出により得られたそれぞれの多次元ベクトルを比較することで探したい画像がそこにあるかを判断することができます。

引用元:OpenCV 3.0.0-dev documentation - Understanding Features

ORBアルゴリズム

ORBについても少し触れておきます。冒頭でも述べたように、ORBはFASTという特徴検出アルゴリズムとBRIEFという特徴記述アルゴリズムを基礎としています。それぞれのアルゴリズムの概要は以下のようなものです。

FAST

FASTはコーナー検出アルゴリズムですが、考え方は次のようなものです。下図左のように中心のピクセルから距離が3であるような円上に存在する16個のピクセルに対して、中心のピクセルより明るいか暗いかを判断します。さらに閾値を設定し、下図右の計算式に当てはめて3つのグループに分類します。このときbrighter(またはdarker)のグループに含まれる点の数が12以上であるようなとき、特徴(コーナー)として判断します。ここで、カラー画像ではなくモノクロ画像として処理していることに疑問を感じた人がいるかもしれませんが、これは計算量を減らすためです。カラー画像よりもモノクロ画像の方が処理するデータ量が小さくなるため、色ではなく形状を判断するような場合においてはモノクロ画像の方が処理速度の面で有利になります。画像処理では一般に、カラーである必要がない場合はモノクロ画像に変換して処理することがほとんどです。

ocv_withglass_fast1
ocv_withglass_fast2

引用元:Edward Rosten and Tom Drummond, “Machine learning for high speed corner detection” in 9th European Conference on Computer Vision, vol. 1, 2006, pp. 430–443.

FASTは感覚的に次のように理解できます。下図で点PA, PB, PCのそれぞれはフラットエリア、エッジ、コーナーの上のピクセルに対応します。PAの場合、中心ピクセルから周囲の16個のピクセルの中心を比べた場合、全ての画像の間で差がでません。また、PBの場合は中心より明るい(または暗い)画像として半分が判別されますが、12個には届きません。PCのような場合にのみ中心より明るい(または暗い)画像が12個以上検出されてきます。ORBの特徴検出にはこのような考え方をベースに改良したアルゴリズムが使われています。

ocv_withglass_fast3

BRIEF

次は特徴抽出のためのBRIEFアルゴリズムです。画像認識ではSIFTやSURFといったアルゴリズムが有名なのですが、計算量が多いという欠点がありました。BRIEFでは局所性鋭敏型ハッシュ(LSH:Locality-sensitive hashing)という手法により、従来のfloat型ではなくbinary型として特徴記述を行うことで計算量の削減に成功しています。

ORBによる画像認識アプリケーションの実装

ここからは本連載のタイトルのGoogle GlassでのORB画像認識アプリケーションの実装です。画像処理では一般に計算量が膨大になるため、実行速度を求める場合はネイティブコードで実装することが多いようです。その慣習に則ってここでも画像処理部はC++により実装しています。実装の手順は以下のとおりです。

  • ネイティブコードのヘッダファイル(*.hpp)を作成
  • ネイティブの実装に必要なコードを書いて(*.cpp, およびAndroid.mk)、ndk-buildにより実行ファイルを生成
  • Androidのコード内でネイティブメソッドを呼び出す処理を実装し、ビルド・実行する

ヘッダファイルの作成

まず、C++のライブラリを呼び出したいクラスファイルの中で呼び出すメソッドの宣言を記述します。1行目は訓練画像(探す対象の画像)を登録するメソッドで、2行目はカメラから取得した画像を渡して探索対象の画像が含まれるかどうかを判断するメソッドです。

ターミナルを開き、プロジェクトのルートディレクトリにてjavahコマンドを実行することによりネイティブのヘッダファイルが作成されます。

ネイティブの実行ファイルの生成

ネイティブのヘッダファイルの生成ができたら、それぞれのメソッドを実装します。まずは、訓練画像を登録するメソッドの実装です。

訓練画像を登録するメソッド

JNIでOpenCV::Mat型の変数をやりとりする場合、ポインタによって変数を受け渡します。今回はひとつの画像のみを認識するだけなので、特徴抽出した結果はグローバル変数に保持しています。12行目で特徴検出と抽出を行っていて、それぞれの引数の意味は以下のとおりです。(リファレンスはこちら

void ORB::operator()(InputArray image, InputArray mask, vector& keypoints, OutputArray descriptors, bool useProvidedKeypoints=false ) const

image 計算対象の画像
mask ビット演算に使用するマスク
keypoints 画像内での特徴の位置などをベクターとして保持する
descriptors 特徴抽出の計算結果を格納
useProvidedKeypoints trueにした場合、計算結果を無視して検出器に渡したvectorデータを使う
画像の検出を行うメソッド

続いて、画像データを検出するメソッドです。訓練画像を登録したときと同じように特徴量計算した後、BFMatcherクラスのmatchメソッドにより比較を行っています。18行目のmatchメソッドの実行により、似た特徴同士のペアの一覧がvector配列として得られます。それぞれの要素の中には特徴のペアのインデックスとどれだけ似ているかの指標となる値が含まれています。ここでは誤検出を防ぐために、閾値を設定しその閾値より小さい値を持った特徴を採用し、その合計が全体の5%に達した時に検出したものとして判断しています。また、21行目で設定した閾値は経験的に得たものとなっています。検出に成功した場合は、画像に"image detected"という表示します。

Android.mkの修正

Android.mkに以下の記述を追記します。Androidから呼び出す際のモジュール名とコンパイル対象となるファイルを指定します。

ndk-buildを実行して実行ファイルを作成したらAndroidの実装に移ります。

Androidからネイティブメソッドを呼び出す

実装が必要な処理は、ライブラリをロードする処理とメソッドを呼び出す処理の2つになります。まず、ライブラリをロードする処理を前回作成したORBDetection.javaに追記します。

次にCameraActivity.javaにてメソッドを呼び出します。JNIでOpenCVのオブジェクトをネイティブとの間でやりとりするには、nativeObjというアドレスを指すlong型変数を使います。訓練画像を登録するメソッドはonResume()メソッド内で呼び出しています。

実行結果

対象画像の検出に成功すると、Java_com_orb_CameraActivity_detectImageメソッド内のcv::putTextが呼び出されて、画面上部に検出したことを示す文字が表示されます。ただし、処理速度は2fps程度とかなり低速でした。次回は実行速度向上のために行った検証内容について書きたいと思います。

ocv_withglass_orb_prtsc_none

エンジニア採用中!私たちと一緒に働いてみませんか?