iOSでCaffeの学習済みモデルを使う

最近opencv_contribにディープラーニング(dnn)モジュールが追加されたようです.
チュートリアルを見るとCaffeのモデルが使えるようなので, iOS frameworkをビルドしてGoogLeNetを使ってみました.

ググると色々とframeworkをビルドする方法が出てくるのですが, OpeCV3.0.0と現在(2015/10/17)のopencv_contribではビルドが通りませんでした. なのでframeworkのビルド方法からまとめてみようと思います.

dnnモジュールを含んだopencv_contrib frameworkのビルド

まず, githubからopencv, opencv_contribをcloneしてopencvは3.0.0タグをチェックアウトします. opencv_contribはmasterで良いと思いますが自分は9818f481bd1556ea8784faa70afeea07c52502feのコミットを使いました.

$ git clone git@github.com:Itseez/opencv.git
$ cd opencv
$ git checkout -b tag-3.0.0 refs/tags/3.0.0
$ cd ..
$ git clone git@github.com:Itseez/opencv_contrib.git
$ cd opencv_contrib
$ git checkout 9818f481bd1556ea8784faa70afeea07c52502fe

opencv_contribを含んだアプリを公開することを考えてnonfreeなもの(xfeature2dモジュール)を除外します. また, saliency, ximageprocモジュールのコンパイルに失敗してしまうのでこれらも除外します. BINGが使えないのは悲しいですが, 今回の目的はCaffeモデルを利用することなので目をつむることにしましょう.

$ git diff
diff --git a/platforms/ios/build_framework.py b/platforms/ios/build_framework.py
index 1acd2b2..f303603 100755
--- a/platforms/ios/build_framework.py
+++ b/platforms/ios/build_framework.py
@@ -53,6 +53,9 @@ def build_opencv(srcroot, buildroot, target, arch):
                 "-DCMAKE_BUILD_TYPE=Release " +
                 "-DCMAKE_TOOLCHAIN_FILE=%s/platforms/ios/cmake/Toolchains/Toolchain-%s_Xcode.cmake " +
                 "-DCMAKE_C_FLAGS=\"-Wno-implicit-function-declaration\" " +
+                "-DBUILD_opencv_saliency=OFF " +
+                "-DBUILD_opencv_ximgproc=OFF " +
+                "-DBUILD_opencv_xfeatures2d=OFF " +
                 "-DCMAKE_INSTALL_PREFIX=install") % (srcroot, target)

     if arch.startswith("armv"):

次にProtocolBuffersのコードを少し修正します. 現在のProtocolBuffersはarm64に対応しているのですが, opencv_contribに含まれているProtocolBuffersは古くarm64でクロスコンパイル時に対応してないアーキテクチャとしてコンパイルが失敗してしまいます. workaroundですが以下のように修正します. ProtocolBuffersは3rdparyライブラリとしてビルドされるだけなので, opencv_contribのビルド時にはProtocolBuffersを除外して, iOSアプリをビルドするときにCocoaPods等で追加することも可能だと思いますが試していません.

$ git diff modules/dnn/3rdparty/protobuf/src/google/protobuf/stubs/platform_macros.h
diff --git a/modules/dnn/3rdparty/protobuf/src/google/protobuf/stubs/platform_macros.h b/modules/dnn/3rdparty/protobuf/src/google/protobuf/stubs/platform_macros.h
index b1df60e..8abfd0c 100644
--- a/modules/dnn/3rdparty/protobuf/src/google/protobuf/stubs/platform_macros.h
+++ b/modules/dnn/3rdparty/protobuf/src/google/protobuf/stubs/platform_macros.h
@@ -49,6 +49,9 @@
 #elif defined(__ARMEL__)
 #define GOOGLE_PROTOBUF_ARCH_ARM 1
 #define GOOGLE_PROTOBUF_ARCH_32_BIT 1
+#elif defined(__aarch64__)
+#define GOOGLE_PROTOBUF_ARCH_AARCH64 1
+#define GOOGLE_PROTOBUF_ARCH_64_BIT 1
 #elif defined(__MIPSEL__)
 #define GOOGLE_PROTOBUF_ARCH_MIPS 1
 #define GOOGLE_PROTOBUF_ARCH_32_BIT 1

このままframeworkのビルドをすると出来上がったframeworkに含まれる静的ライブラリにはdnnモジュールのシンボルが含まれません. なのでopencv_contrib_worldモジュールにすべてのモジュールが含まれるようにCMakeLists.txtを修正します. 他のモジュールもいくつか抜けていたので, frameworkひとつにまとめるため一緒に追加してしまいます.

$ git diff modules/contrib_world/CMakeLists.txt modules/dnn/CMakeLists.txt modules/saliency/CMakeLists.txt modules/ximgproc/CMakeLists.txt
diff --git a/modules/contrib_world/CMakeLists.txt b/modules/contrib_world/CMakeLists.txt
index dafe6d5..e063525 100644
--- a/modules/contrib_world/CMakeLists.txt
+++ b/modules/contrib_world/CMakeLists.txt
@@ -5,11 +5,15 @@ set(BUILD_opencv_contrib_world_INIT OFF) # disabled by default

 # add new submodules to this list
 set(OPENCV_MODULE_CHILDREN
+  adas
+  aruco
   bgsegm
   bioinspired
   ccalib
   cvv
   datasets
+  dnn
+  dpm
   face
   latentsvm
   line_descriptor
@@ -17,6 +21,8 @@ set(OPENCV_MODULE_CHILDREN
   reg
   rgbd
   saliency
+  stereo
+  structured_light
   surface_matching
   text
   tracking
diff --git a/modules/dnn/CMakeLists.txt b/modules/dnn/CMakeLists.txt
index c2fdea9..91f5f5e 100644
--- a/modules/dnn/CMakeLists.txt
+++ b/modules/dnn/CMakeLists.txt
@@ -1,6 +1,5 @@
 cmake_minimum_required(VERSION 2.8)
 set(the_description "Deep neural network module. It allows to load models from different frameworks and to make forward pass")
-set(OPENCV_MODULE_IS_PART_OF_WORLD OFF)

 ocv_add_module(dnn opencv_core opencv_imgproc WRAP python matlab)
 ocv_warnings_disable(CMAKE_CXX_FLAGS -Wno-shadow -Wno-parentheses -Wmaybe-uninitialized -Wsign-promo -Wmissing-declarations -Wmissing-prototypes)
@@ -58,4 +57,4 @@ if(${the_module}_BUILD_TORCH_TESTS)
                         COMMAND th ${CMAKE_CURRENT_SOURCE_DIR}/testdata/dnn/torch/torch_gen_test_data.lua
                         WORKING_DIRECTORY  $ENV{OPENCV_TEST_DATA_PATH}/dnn/torch )
     add_definitions(-DENABLE_TORCH_TESTS=1)
-endif()
\ No newline at end of file
+endif()
diff --git a/modules/saliency/CMakeLists.txt b/modules/saliency/CMakeLists.txt
index 31f09f9..ba6b216 100644
--- a/modules/saliency/CMakeLists.txt
+++ b/modules/saliency/CMakeLists.txt
@@ -1,3 +1,2 @@
 set(the_description "Saliency API")
-set(OPENCV_MODULE_IS_PART_OF_WORLD OFF)
 ocv_define_module(saliency opencv_imgproc opencv_highgui opencv_features2d WRAP python)
diff --git a/modules/ximgproc/CMakeLists.txt b/modules/ximgproc/CMakeLists.txt
index 91ed65e..7348690 100644
--- a/modules/ximgproc/CMakeLists.txt
+++ b/modules/ximgproc/CMakeLists.txt
@@ -1,5 +1,4 @@
 set(the_description "Extended image processing module. It includes edge-aware filters and etc.")
-set(OPENCV_MODULE_IS_PART_OF_WORLD OFF)
 ocv_define_module(ximgproc opencv_imgproc opencv_core opencv_highgui opencv_calib3d WRAP python)

 target_link_libraries(opencv_ximgproc)

ここでframeworkのビルドコマンドを実行します.

$ python opencv/platforms/ios/build_framework.py --contrib opencv_contrib opencv_framework

opencv_frameworkディレクトリにframeworkが吐かれます. ですが, opencv2_contrib.frameworkとして吐かれるため, Xcodeにframeworkを追加した時ヘッダをopencv2から始まる相対パスで参照することができず, ヘッダが存在しないエラーの嵐に悩まされることになります. これを回避するためヘッダファイル中の#includeを書き換えます. また, ProtocolBuffersはUniversalバイナリとしてまとめられないので, 利用しやすいようにlipoでまとめてしまいます. これらを行うため以下のシェルスクリプトを作りました. opencv_frameworkディレクトリで実行してください.

#!/bin/bash

# Replace include path

modules=(
  adas
  aruco
  bgsegm
  bioinspired
  ccalib
  cvv
  datasets
  dnn
  dpm
  face
  latentsvm
  line_descriptor
  optflow
  reg
  rgbd
  saliency
  stereo
  structured_light
  surface_matching
  text
  tracking
  xfeatures2d
  ximgproc
  xobjdetect
  xphoto
)

headers=`find ./opencv2_contrib.framework/Headers/* -name *.hpp`

for module in ${modules[@]}; do
  echo $module
  sed -i "" -e "s/opencv2\/${module}/opencv2_contrib\/${module}/g" $headers
done


# Make universal library

lipo -create ./build/iPhoneOS-armv7/3rdparty/lib/Release/liblibprotobuf.a ./build/iPhoneOS-armv7s/3rdparty/lib/Release/liblibprotobuf.a ./build/iPhoneOS-arm64/3rdparty/lib/Release/liblibprotobuf.a ./build/iPhoneSimulator-i386/3rdparty/lib/Release/liblibprotobuf.a ./build/iPhoneSimulator-x86_64/3rdparty/lib/Release/liblibprotobuf.a -output libprotobuf.a

わりと力技な感があるので, dnnを含んだopencv_contrib frameworkをもっとスマートにビルドする方法を知っている方がいればぜひ教えていただきたいです.

dnnモジュールを利用する

上記の手順を行うとopencv_frameworkディレクトリlibprotobuf.aとopncv2_contrib.frameworkが作成されていると思うので, これをアプリのXcodeプロジェクトに追加してください. また, ここから3.0.0のopencv.frameworkをダウンロードし一緒にプロジェクトに追加します. ダウンロードしたopencv.frameworkに含まれる静的ライブラリにはLLVMビットコードが含まれていないので, ビルドするときはBuild SettingsからEnable BitcodeをNoにしてください.

サンプルコードはチュートリアルに載っているものとほとんど同じなので割愛します. 自分はSwiftで利用したかったので, Objective-C++でラッパークラスを作ってBridging Headerを追加しました. ひとつ注意なのはUIImageToMat()でUIImageからcv::Matに変換する際, カラー画像は4チャネルのMatに変換されることです. GoogleLeNeは3チャネルのBlobを入力とするので, BGRフォーマットに変換する必要があります.

試しに認識させる画像と分類結果を表示するアプリを作ってiPhone6実機で動かしてみました. iOS Simulatorでも問題なく動きます.
IMG_0313
以上です.

広告
カテゴリー: Code, Computer Vision, Machine Learning, OpenCV | コメントをどうぞ

高速な楕円検出アルゴリズムを実装してみた

[1]の論文に書かれている楕円検出アルゴリズムをpythonで実装してみました. 論文ではSamsung Galaxy S2を使っていて, C++実装でJNI経由で呼び出すと, 800×440の画像サイズで10FPSほどの性能が出るそうです. 現行のスマホではもっと性能がでると思います.

python実装なので論文ほどの性能は出ないです. 過度な期待はしないでください.
https://github.com/horiken4/ellipse-detection

以下, 大まかなアルゴリズムを説明したいと思います.

1. 円弧を4つのクラスに分ける
グレースケール画像の入力画像にガウシアンフィルタをかけ, Cannyオペレータによりエッジ検出をします. また, Sobelオペレータにより各画素のx, y方向微分値dX, dYを求めます.

ellipse-preprocess

Cannyオペレータによって検出されたエッジ画素に関して, dY/dX>0なら上図(a)の{2, 4}, dY/dX<0なら上図(a)の{1, 3}の円弧のものだろうと推測できるので, すべての画素をクラス{2, 4}, {1, 3}にクラス分けします.

クラス分けしたあとは8近傍のラベリングを行い, 円弧らしいセグメントを検出します. ここで検出されたセグメントのうち長さが短すぎるものや, 直線に近いものは楕円を構成する円弧ではないと判断し捨てます.

検出されたそれぞれのセグメントについてバウンディングボックスを考え, セグメントの下と上の面積A, Bを求めます(上図(b)). AがBより大きければ上に凸, そうでなければ下に凸であると判断します.

まとめると

  • クラス{2, 4}のセグメントで上に凸→クラス2
  • クラス{2, 4}のセグメントで下に凸→クラス4
  • クラス{1, 3}のセグメントで上に凸→クラス1
  • クラス{1, 3}のセグメントで下に凸→クラス3

となり各セグメントを4つのクラスに分けることができます.

2. セグメントの選択と楕円の検出
セグメントが4つのクラスに分けられましたが, このままではどのセグメントがひとつの楕円を構成するのかわかりません. 楕円のパラメタは3つの円弧があれば推定可能なので, 楕円を構成しそうなセグメントの3つ組(e_i, e_j, e_k)を探します.

セグメントの個数をnに対して\binom{n}{3}の組み合わせをすべて試すのは効率が良くないので, いくつかの制約から探索範囲を狭くします. まず, 上図(a)を見るとわかりやすいですが, (1, 2, 4), (2, 3, 1), (3, 4, 2), (4, 1, 3)のクラスのセグメントの組しか楕円を構成しないことがわかります, また, 例えば(1, 2, 4)の場合はクラス1のセグメントに対して, クラス2のセグメントは左に, クラス4のセグメントは下になければならない, といったような制約があります. これらの条件を満たす3つ組をすべて列挙します.

次に, 3つ組に対して, 楕円の中心を推定します. 3つ組に含まれる2つの隣り合う円弧のをつなぐ線分の中心を通る直線の交点が楕円の中心になるので(下図(a)), 2つのペア(e_i, e_j), (e_i, e_k)から求めた楕円の中心が十分近ければ, この3つ組は楕円を構成する3つ組だと判断し, 下図(b)で示してある様々なパラメタを求めます. そうでない場合, この3つ組は捨てます.

ellipse-triplet

3. 楕円のパラメタの推定
2. で求めたスロープの傾きや楕円の中心候補などから楕円のパラメタを求めます. ごちゃごちゃしているので詳しく知りたい方は論文のParameters Estimationの章を参照してください. パラメタのとりうる範囲を離散化して投票制によってパラメタを決定しています.

最終的な楕円のパラメタが推定されたあと, 検出された楕円のスコアを以下の式で求めます.
score = \frac{number\ of\ points\ lying\ on\ the\ actual\ contour}{|e_i| + |e_j| + |e_k|}
つまり, 推定された楕円にフィットするセグメントの画素数の割合を求めています.

4. 後処理
アルゴリズムの性質上, ひとつの楕円に対して4つ以上のセグメントが検出された場合, 若干パラメタの違う楕円が複数検出されることになるため, これらの楕円をマージします. 楕円の類似度を[2]に記載されている方法で求め, 同じ楕円と判断された場合はスコアの大きい楕円を採用します.

ざっくりと説明しましたが, だれかの助けになれば幸いです. スマホでカメラ自校正させたいとか, メトリック復元したいとかいったときに使えるんじゃないでしょうか。

参考文献

[1] M. Fornaciari and A. Prati, “Very Fast Ellipse Detection for Embedded Vision Applications”
[2] D. Prasad and M. Leung, “Clustering of ellipses based on their distinctiveness: An aid to ellipse detection algorithms”
[3] A. Fernandes, “A Correct Set of Equations for the Real-time Ellipse Hough Transform Algorithm”
カテゴリー: Code, Computer Vision, OpenCV | コメントをどうぞ

goroutineとchannelでGoらしい?Hopfield network

概要

Hopfield networkとはすべてのニューロンが互いに接続している人工ニューラルネットワーク(ANN)の1つです. 完全グラフをイメージするとわかりやすいと思います. 連想記憶のモデルとして考えられたり, 組み合わせ最適化問題を解くために利用されたりします.

Hopfield networkは他のANNと同様に, あるニューロンの出力は他のニューロンからの入力と結合荷重の内積より決定されます.
ひとつのニューロン出力の更新規則は以下の通りで, ニューロンをランダムに選びそのニューロンに適用するということを繰り返します.

\displaystyle{  \begin{array}{rcl}  v_i & = & f(s_i - \theta_i) \\  s_i & = & \sum_j w_{ij}v_j \\  f(x) & = & \left\{  \begin{array}{cc}  1& (x>0) \\  -1 & (x \leq 0)  \end{array}  \right .  \end{array}  }

ここで, v_i, \theta_i, w_{ij}はそれぞれニューロンiの出力, しきい値, ニューロンi,j間の結合荷重です. ただし, w_{ii}=0, w_{ij}=w_{ji}という制約があります(ニューロン自分自身のからの入力は無く, ニューロン間の結合荷重は対称ということ)

普通に書くならシングルスレッドでループを回して1つずつのニューロンを更新していくのですが, ここではそうではなくGoらしい(というかGo独特の機能を使った)書き方をしたいと思います.

Goにはgoroutineと呼ばれる軽量スレッドとgoroutine間のメッセージキューであるchannelと呼ばれる機能があります. このgoroutineひとつをニューロンひとつ, channelひとつをニューロン間の接続ひとつ, とみなしHopfield networkの更新を行うというとても無駄なことをします. イメージとしてはGPUのシェーダプログラミングに近い感じですね. 今回は連想記憶の実験をしてみようと思います.

ここで言う連想記憶とはネットワークに覚えさせたパターンと類似するパターンをネットワークに入力すると, 覚えさせたパターンが想起されるということです. 例えば画像の一部を見ただけで全体をイメージできたり, 次々と類似するものを連想していくことができたりなど, 人間の脳にも備わっている能力のひとつです.

ネットワークの学習(パターンをネットワークに覚えさせる)

詳しい説明は省略しますが, Hopfield networkはニューロンの更新を繰り返すことで以下のエネルギー関数を必ず減少させることがわかっています.
E \left(\bf v \right) = -\frac{1}{2} \sum_i^N \sum_j^N w_{ij} v_i v_j + \sum_i \theta_i v_i

そこで, 覚えさせたいパターンがエネルギー関数の極小値になるようにネットワークの結合荷重を調整するわけですね. パターンを覚えさせる場合結合荷重は以下のように決定します.

w_{ij} = \left\{  \begin{array}{ll}  \sum_{\bf v \in  V} v_i v_j  & (i \neq j) \\  0 & (i = j)  \end{array}  \right .

ここで, V = \{ {\bf v}_1, {\bf v}_2, \cdots {\bf v}_P \}はネットワークに覚えさせるパターン{\bf v} \in \{-1, 1\}^Nの集合です. これはニューロンが同じ出力をしていた場合それらのニューロン間の結合荷重を増加させそうでない場合は減少させる, ヘブ則という学習則で結合荷重を決定していることになります. ネットワークに覚えさせることのできるパターンの個数は, ニューロン数Nの15%程度となっています.

Goでの実装

ニューロンひとつを表現するNeuron構造体は以下のように定義しています.

type Neuron struct {
    id        int
    weights   map[*Neuron]float32 // 結合荷重
    inAxons   map[*Neuron]Axon    // 入力channel
    outAxons  map[*Neuron]Axon    // 出力channel
    v         float32             // このニューロンの出力値
    vChan     chan float32        // 出力値を指定するchannel
    th        float32            
    trainMode bool           
}

Hopfield networkは完全グラフとして表現できるので, ニューロン間の全結合を表現するためN(N-1)個の容量1のchannelを用意します. また, ニューロンに値を流し込むN個の容量1のchannel vChanを作成します. あるNeuronの出力channelはあるNeuronの入力channelになるので, それを考慮してNeuron間をchannelで結合する(NeuronのinAxons, outAxonsを設定する)関数を定義し, Hopfield networkを構築します. 結果として, 例えばニューロンが4個のHopfield networkは以下の図のように構築されます.

gopfield_channel

想起をする際は, vChanでNeuronの出力値を設定したあと, Mainのgoroutineが全Neuronのgoroutineを起動し, 概要で示した更新規則を適用していきます. ニューロン間の結合は容量1のchannelなので, あるニューロンは自身に出力する他のニューロンの更新作業がすべて終わらないとブロックされ更新を行うことができなくなります. つまり, すべての入力がそろったニューロンから随時goroutineによって出力の更新処理が行われていくことになります. これはランダムにニューロンを選択する手順とみなすことができます.

n.v = <-n.vChan                                                          
                                                                         
for it := 0; it < iter; it++ {                                                                                                                    
    for neuron, axon := range n.outAxons {                               
        axon <- n.v                                                      
    }                                                                    
                                                                         
    var s float32                                                        
    for neuron, axon := range n.inAxons {                                
        s += n.weights[neuron] * <-axon                                  
    }                                                                    
                                                                         
    s -= n.th                                                            
                                                                         
    var v float32 = -1                                                   
    if s > 0 {                                                           
        v = 1                                                            
    }                                                                    
    n.v = v                                                              
}                                                                        

学習をする際は, Neuronを学習モードにして更新を行うgoroutineをすべて起動し, 各パターンに対応するNeuronの出力をvChanに流し込む, という流れで行います. vChanで流し込まれた出力値がNeuronに設定され, 全出力channelに出力されます. 各入力channelから他のNeuronの出力を受け取ったNeuronは自身の出力値とその値を比べ, ヘブ則により結合荷重を調整します. これは学習パターンの個数だけ繰り返されます.

finish := make(chan bool, len(h.Neurons))

for _, neuron := range h.Neurons {
    neuron.Run(len(pats), finish)
}

for _, pat := range pats {
    for i, v := range pat {
        h.Neurons[i].Feed(v)
    }
}

for i := 0; i < len(h.Neurons); i++ {
    <-finish
}

ニューロンひとつの更新処理をひとつのgoroutineで行ったことにより, 学習するときも想起するときも更新の時間軸方向に関するループだけがgoroutineに含まれ, 空間方向に関するループが存在しないことに注意してください.

動かしてみる

N=25個のニューロンからなるネットワークを構築し以下の3つのパターンを覚えさせます.

pats := [][]float32{
    []float32{
        1, 1, 1, 1, 1,
        -1, 1, -1, 1, -1,
        1, 1, 1, 1, 1,
        1, -1, 1, -1, 1,
        1, 1, 1, 1, 1,
    },
    []float32{
        -1, -1, 1, 1, 1,
        1, 1, 1, -1, -1,
        -1, -1, 1, 1, 1,
        1, 1, 1, -1, -1,
        -1, -1, 1, 1, 1,
    },
    []float32{
        1, -1, 1, -1, 1,
        -1, 1, -1, 1, -1,
        1, -1, 1, -1, 1,
        -1, 1, -1, 1, -1,
        1, -1, 1, -1, 1,
    },
}

その状態でニューロンに出力値を与えパターンが想起されるかためしてみます. それぞれの初期状態のパターンとして以下のパターンを入力します. これは覚えさせたパターンの20%を反転させたものになっています.

initPats := [][]float32{
    []float32{
        -1, 1, 1, 1, 1,
        -1, -1, -1, 1, -1,
        1, -1, 1, 1, 1,
        1, -1, -1, -1, 1,
        1, 1, 1, -1, 1,
    },
    []float32{
        -1, -1, 1, -1, 1,
        1, -1, 1, -1, -1,
        -1, 1, 1, 1, 1,
        1, 1, 1, 1, -1,
        -1, -1, 1, -1, 1,
    },
    []float32{
        1, -1, -1, -1, 1,
        -1, 1, 1, 1, -1,
        1, 1, 1, -1, 1,
        -1, 1, -1, -1, -1,
        1, -1, -1, -1, 1,
    },
}

ニューロンの出力が-1, 1の場合はそれぞれ白丸, 黒丸で表示しています. 結果は以下に示したように, 覚えさせたパターンを想起できていることがわかります.

Connectting between neuron 0 to neuron 1
Connectting between neuron 0 to neuron 2
Connectting between neuron 0 to neuron 3
Connectting between neuron 0 to neuron 4
Connectting between neuron 0 to neuron 5
Connectting between neuron 0 to neuron 6

...

neuron 14 : input from 11 before
neuron 14 : input from 11 after
neuron 14 : input from 16 before
neuron 14 : input from 16 after
neuron 14 : input from 19 before
neuron 14 : input from 19 after
neuron 14 : finish
●●●●●
○●○●○
●●●●●
●○●○●
●●●●●
energy = -284
neuron 0 : start
neuron 0 : it = 0
neuron 0 : output to 7 before
neuron 0 : output to 7 after
neuron 0 : output to 9 before
neuron 0 : output to 9 after
neuron 0 : output to 18 before
neuron 0 : output to 18 after

...

neuron 16 : input from 0 before
neuron 16 : input from 0 after
neuron 16 : input from 1 before
neuron 16 : input from 1 after
neuron 16 : input from 2 before
neuron 16 : input from 2 after
neuron 16 : finish
○○●●●
●●●○○
○○●●●
●●●○○
○○●●●
energy = -280
neuron 0 : start
neuron 0 : it = 0
neuron 0 : output to 7 before
neuron 0 : output to 7 after
neuron 0 : output to 9 before
neuron 0 : output to 9 after
neuron 0 : output to 18 before
neuron 0 : output to 18 after

...

neuron 1 : input from 0 before
neuron 1 : input from 0 after
neuron 1 : input from 4 before
neuron 1 : input from 4 after
neuron 1 : input from 21 before
neuron 1 : input from 21 after
neuron 1 : finish
●○●○●
○●○●○
●○●○●
○●○●○
●○●○●
energy = -280

まとめ

goroutineをひとつのニューロン, ニューロン間の結合をchannelとして表現することで, Hopfield networkを構築するというすごく無駄なことをしました. ネットワーク越しにchannelを扱えるようにするnetchanというパッケージもあるようなので, 複数のコンピュータを用いて上記の方法で巨大なHopfield networkを構築してみるのも, もっと無駄で面白いかもしれません.

ソースはhttps://github.com/horiken4/gopfieldに置いてあるので, 気になった人は参照してください.

カテゴリー: Code, Go, Machine Learning | コメントをどうぞ

OpenCVでBINGを使う

BING(http://mmcheng.net/bing/)は画像中からオブジェクトらしい矩形領域を抽出することができる手法で, 物体認識の前処理などで使えます.
論文によるとObjectness上位1000件で96%の検出率と他のstate-of-the-artより良く, しかも1000倍高速に動作するとのことなので,
色々と使えそうですね. 試してみる価値がありそうです.

BINGがいつのまにかopencv_contrib(https://github.com/itseez/opencv_contrib)で実装されていたので実際に使ってみました.

まず, opencv_contribのREADME.mdに従ってopencv_contribが利用可能なOpenCVをビルドしてください.
OpenCV 3.0 betaとopencv_contribの組み合わせではビルドが通らないので, OpenCVのmasterブランチ(https://github.com/Itseez/opencv)を使用することをおすすめします.

次にBINGの学習を行うため, オリジナルのソースをMacでビルド可能に修正したリポジトリをforkします.
自分の環境はOpenCV 3.0 alphaなのでそれに合わせて修正し, OenCVのBINGでロード可能なyml.gz形式でモデルを保存するように修正しました.
修正済みソースはhttps://github.com/horiken4/BING-Objectnessにおいてあります. clone後, README.mdに従ってVOCデータセットを適切に配置してください.
makeするとBINGが作成されるので, それを実行すると学習が開始されまず. 学習済みモデルはVOC2007/Resultsに保存されます.

$ cd Objectness
$ ./BING
$ ls ../VOC2007/Results/
BBoxesB2W8I ObjNessB2W8HSV.wS2 ObjNessB2W8I.idx.yml.gz ObjNessB2W8I.xP ...

最後に, 学習済みモデルを読み込んでオブジェクトらしい領域を抽出してみます.
画像中のオブジェクトらしい矩形をObjectnessが高い方から1000個描画するテストプログラム(https://github.com/horiken4/opencv-bing-test)を作ったのでclone後, ビルドしてください.
例えば, 以下のコマンドで実行できます.

$ cmake .
$ make
$ ./bin/opencv_bing_test $HOME/repos/BING-Objectness/VOC2007/Results $HOME/repos/BING-Objectness/VOC2007/JPEGImages/000800.jpg

bing

上位1000件を描画してもわかりづらいですねw実際にはこれらの矩形に対して分類を行うことになると思います.

以上です.

カテゴリー: Code, Computer Vision, OpenCV | 3件のコメント

画像間ホモグラフィによる平面画像の補正

同一平面を撮影した画像が2枚以上得られた場合に画像間ホモグラフィから補正画像を作成する方法を考える. ただし, カメラの内部行列は既知とする.
参照カメラ(フレーム1とする)の姿勢ホモグラフィを求め, その逆行列を用いることで補正画像を得ることができる. ここでは, その姿勢ホモグラフィを非線形最適化により求める手法を示す.

簡単化のため平面はz=0と仮定すると, 平面上の点\left(x, y, 0\right)は正規化カメラ座標系での点\left(u, v\right) に以下のように射影される.
\displaystyle{  \left(    \begin{array}{c}      u \\      v \\      1    \end{array}  \right)    \sim    \left(R | {\bold t} \right)   \left(    \begin{array}{c}      x \\      y \\      0 \\      1    \end{array}  \right)    =    \left({\bold r}_1 | {\bold r}_2 | {\bold t} \right)   \left(    \begin{array}{c}      x \\      y \\      1    \end{array}  \right)  }

ここで, {\bold r}_1, {\bold r}_2はそれぞれ, 外部行列の1, 2列ベクトルである.
C=\left({\bold r}_1 | {\bold r}_2 | {\bold t} \right)とおくと, 回転行列は直交行列なので,

\displaystyle{  C^T C =   \left(    \begin{array}{ccc}      1 & 0 & \cdot \\      0 & 1 & \cdot \\      \cdot & \cdot & \cdot    \end{array}  \right)  }

となり, 任意の姿勢ホモグラフィCで成り立つ.
また, C_1を参照カメラの姿勢ホモグラフィ, H_{i, 1}をカメラ1-カメラi間のホモグラフィとすると

\displaystyle{  C_i = H_{i, 1} C_1  }

より,

\displaystyle{  C_i^T C_i = \left(C_1 H_{i,1}\right)^T H_{i, 1} C_1 =   \left(    \begin{array}{ccc}      1 & 0 & \cdot \\      0 & 1 & \cdot \\      \cdot & \cdot & \cdot    \end{array}  \right)  }

を満たさなければならない. そこで, この関係を用いて1つのホモグラフィに関するコスト関数をe_i(C_1) = \left(a_{1, 2} / a_{1, 1}\right)^2  +  \left(a_{2, 2} / a_{1,1} - 1 \right)^2と定義する. 画像間ホモグラフィが複数得られたとすると, 最小化すべきコスト関数は

\displaystyle{  e(C_1) = \sum_i e_i(C_1)  }

となる. カメラ1の座標(平面の法線ベクトルをz軸とする世界座標系表現)を\left(0, 0, -1 \right)に固定すると, C_1はx, z軸回転の2つの自由度を持つことから
このコスト関数のパラメタはそれぞれの回転角度\alpha, \betaとなる. ただし, 平面がカメラの背面に存在する解を防ぐため, \alpha \in \left(-\pi/2, \pi/2 \right),  \beta \in \left[-\pi/2, \pi/2 \right)の制約を設ける. ただし, 平面の法線ベクトルに沿った並進または, 回転によって得られた画像間ホモグラフィはどのようなパラメタに関してもコスト関数の値が0となるため, それらの画像間ホモグラフィのみでは正しい解が得られないことに注意.

以下の画像は2枚の画像で姿勢ホモグラフィを計算する実験.

カメラ1, の画像から特徴点(AKAZE)を抽出
frame1

frame2

マッチング, 画像間ホモグラフィの推定
matches

補正画像
rectified_image

Practical Planar Metric Rectification

カテゴリー: Computer Vision | コメントをどうぞ

踊ってみた動画から姿勢推定する実験

たまに, ニコニコ動画で踊ってみた動画とか, MMDで作られた動画とか見るんですけど, それを見てる時に「ボーカロイドの曲に合わせて踊っている人沢山いるしモーションを踊ってみた動画から生成できればいいんじゃないか?」と思ったわけですね. あとは, MMDで3次元のアニメーション作るのって初心者には難しくて, だったら2次元の動画から作れたらいいよねという思いもありました. Kinectにも対応しているようですけど, そもそも踊れないとダメですしね(´・ω・`)

一応, 目的は踊っている人の関節位置を動画の毎フレーム, ユーザが指定することで, 3次元の姿勢を推定することとします. しかも, カメラの内部, 外部パラメタは未知の状態から推定する必要があります. 当然, 使用したカメラの情報までうpしている人はいないので.

まずは, 人体のモデルを定義しないと話にならないので, 定義します. 骨は17本とします.
human-model
ユーザはこのモデルの関節に当たる部分を動画の1フレームごとに指定していきます.

カメラは弱中心射影カメラを仮定します. いくつか踊ってみた動画を見た感じ, ほぼ全身が映るように撮影されているようなので, カメラから人体までの距離が十分大きいと仮定できるんじゃないでしょうか.
弱中心射影カメラを仮定したことにより, カメラ座標系での点{\bf p} = \left(x, y, z\right)^Tは, {\bf x} = \left(u, v\right)^T =  \left(sx, sy\right)^Tに投影されます. sは未知の内部パラメタ, スケールファクタです.

上記を踏まえて, 拘束を導きます. i番目の骨の長さをl_iとすると, 以下の拘束が得られます.
\displaystyle{  l_i^2 = \left| {\bf p}_{i_1} - {\bf p}_{i_2}\right|^2, i=1, \ldots, B  }
ここで, B=17は骨の本数, {\bf p}_{i_1}, {\bf p}_{i_2}i番目の骨の始点, 終点です. これを画像平面上の点{\bf x}を使って書き直すと.
\displaystyle{  dz_i^2 = l_i^2 - \frac{\left| {\bf x}_{i_1} - {\bf x}_{i_2}\right|^2}{s}, i=1, \ldots, B  }
が得られます. ここで, dz_iは始点, 終点の相対的な深さです. この拘束が毎フレーム分得られるので, フレーム数をKとすると, KBの拘束が得られることになります. また, 人体は腕, 脚が左右で同じ長さであるはずなので, l_{i_1}^2 = l_{i_2}^2, i=1, \ldots, 7 の拘束が下腕, 上腕, 肩, 股関節, 上脚, 下脚, 足について合計7つ得られます. さらに, 人体モデルの図で点線で表した部分(以下, 仮想骨と呼びます)の長さはあらゆる姿勢でほぼ変化しないということがモーションキャプチャ関連の先行研究で分かっているので, 1フレームにつき以下の4つの拘束が得られます. あらゆる姿勢で仮想骨の長さは変わらない(仮想骨を1辺とする三角形は剛体とみなせる)ので, 仮想骨を1辺とする三角形を考えて, 1辺の相対深度は他の2辺の相対深度の和になってなければならないという条件と最初に導出した拘束を使って導出します.
\displaystyle{  \left( l_{B,C}^2 - \frac{\left| {\bf x}_B - {\bf x}_C \right|^2}{s^2} - dz_{A,B}^2 - dz_{A,C}^2 \right)^2 = 4 dz_{A,B}^2 dz_{A,C}^2  }
\displaystyle{  \left( l_{A,E}^2 - \frac{\left| {\bf x}_A - {\bf x}_E \right|^2}{s^2} - dz_{A,D}^2 - dz_{D,E}^2 \right)^2 = 4 dz_{A,D}^2 dz_{D,E}^2  }
\displaystyle{  \left( l_{A,F}^2 - \frac{\left| {\bf x}_A - {\bf x}_F \right|^2}{s^2} - dz_{A,D}^2 - dz_{D,F}^2 \right)^2 = 4 dz_{A,D}^2 dz_{D,F}^2  }
\displaystyle{  \left( l_{E,F}^2 - \frac{\left| {\bf x}_E - {\bf x}_F \right|^2}{s^2} - dz_{E,D}^2 - dz_{D,F}^2 \right)^2 = 4 dz_{E,D}^2 dz_{D,F}^2  }
ここで, 仮想骨の長さl_{B,C}, l_{A,F}, l_{A,E}, l_{E,F} を新たな未知数として導入しました.

一旦未知数と拘束の個数をまとめておきます. 未知数は各骨, フレームごとの相対深度{\bf dz}^2 \in R^{KB} , 各フレームごとのカメラのスケールファクタ{\bf s}^2 \in R^K, 骨長さ{\bf l}^2 \in R^{B-1}, 仮想骨長さ{\bf e}^2 = \left(l_{B,C}^2, l_{A,F}^2, l_{A,E}^2, l_{E,F}^2 \right)^Tなので, KB + K + B + 3個となっています. 骨長さの絶対的な長さは単眼カメラを使用しているため求めることは不可能なので, 1番目の骨の長さを1として他の骨の長さを求めます. そのため, 骨長さの未知数はB-1個となっています.
一方拘束は, 各骨, フレームごとに
\displaystyle{  dz_i^2 = l_i^2 - \frac{\left| {\bf x}_{i_1} - {\bf x}_{i_2}\right|^2}{s}, i=1, \ldots, B  }
KB個, l_{i_1}^2 = l_{i_2}^2が7個, 各フレームごとに
\displaystyle{  \left( l_{B,C}^2 - \frac{\left| {\bf x}_B - {\bf x}_C \right|^2}{s^2} - dz_{A,B}^2 - dz_{A,C}^2 \right)^2 = 4 dz_{A,B}^2 dz_{A,C}^2  }
\displaystyle{  \left( l_{A,E}^2 - \frac{\left| {\bf x}_A - {\bf x}_E \right|^2}{s^2} - dz_{A,D}^2 - dz_{D,E}^2 \right)^2 = 4 dz_{A,D}^2 dz_{D,E}^2  }
\displaystyle{  \left( l_{A,F}^2 - \frac{\left| {\bf x}_A - {\bf x}_F \right|^2}{s^2} - dz_{A,D}^2 - dz_{D,F}^2 \right)^2 = 4 dz_{A,D}^2 dz_{D,F}^2  }
\displaystyle{  \left( l_{E,F}^2 - \frac{\left| {\bf x}_E - {\bf x}_F \right|^2}{s^2} - dz_{E,D}^2 - dz_{D,F}^2 \right)^2 = 4 dz_{E,D}^2 dz_{D,F}^2  }
4K個, 合計KB+4K+7個の拘束があります. 拘束が未知数より多くなければ解けないので, K \geq 5以上のフレームが必要となります.

上記の拘束を用いてLevenberg-Marquardt法により解を求めます. 拘束の左辺を右辺の差が0にならなければならないので, 目的は
\displaystyle{  \arg \min_{{\bf dz}^2, {\bf s}^2, {\bf l}^2, {\bf e}^2} E_p \left({\bf l}^2, \frac{1}{{\bf s}^2}, {\bf dz}^2 \right) + \lambda_1 E_s \left({\bf l}^2\right)+ \lambda_2 E_r \left({\bf e}^2, \frac{1}{{\bf s}^2}, {\bf dz}^2\right)  }
となります. ここで, E_p, E_s, E_rはそれぞれ上記の3つの拘束の右辺を左辺に移項して2乗したものです. \lambda_1, \lambda_2はそれぞれ, どちらの拘束を優先して最適化を行うかというパラメタです.

ここまでで, 骨長さと相対深度, スケールファクタがわかりましたが, 各未知数を2乗したまま解いたので相対深度の符号が定まらず, 姿勢を一意に決定することができません. そこで, 関節の再投影誤差が最小になるように関節角度を求めることで, 最終的な姿勢を求めます. 関節の自由度は頭2, 首2, 背骨3, 左右の鎖骨2, 上腕骨3, 肘1, 大腿骨3, 膝1, 足首2の合計31なので, 各フレームを考えると未知数は{\bf q}_k \in R^{31}, k=1, \ldots, Kとなります.
関節角と上記で求めた骨長さを用いると, i番目の関節の座標は{\bf p}_{k,i} = {\bf f}_i \left({\bf q}_k, {\bf l} \right)となります. ここで, {\bf f}_ii番目の関節をカメラ座標系に座標変換する関数とします. そうすると, 関節の再投影誤差は
\displaystyle{  E_p^{\prime} = \sum_{k=1}^K \sum_{i=1}^B \left(s_k {\bf f}_i \left({\bf q}_k, {\bf l} \right)  - {\bf x}_{k,i} \right)^T  \left(s_k {\bf f}_i \left({\bf q}_k, {\bf l} \right)  - {\bf x}_{k,i} \right)  }
となります. また, 仮想骨長さは各フレームで変化しないので
\displaystyle{  E_r^{\prime} = \sum_{k=1}^K \sum_{i=1}^4 \left(   \left|    {\bf f}_{i_1} \left({\bf q}_k, {\bf l} \right)  -  {\bf f}_{i_2} \left({\bf q}_k, {\bf l} \right)   \right|^2 - e_i   \right)^2  }
となります. ここで, {\bf f}_{i_1},  {\bf f}_{i_2}はそれぞれ, i番目の仮想骨の始点, 終点の座標を表しています. これらの拘束を使うと, 関節角には可動域の上限, 下限{\bf q}^l, {\bf q}^uがあることから, 目的は
\displaystyle{  \arg \min_{\{{\bf q}_k\}} E_p^{\prime} +\lambda_1 E_r^{\prime} s.t. {\bf q}^l \leq {\bf q}_k \leq {\bf q}^u  }
となります. これをLevenberg-Marquardt法で解くことで, 全てではないですが, たいていの場合は姿勢を一意に決定することができます. {\bf q}_kの初期値は, 相対深度の符号をランダムに決定した値{\bf dz}と骨長さ{\bf l}を使って逆運動学により求めたものを使います.

とりあえずハロ/ハワユの踊ってみた動画から姿勢を推定してみました. 以下のようにフレームを移動しながら, マウスで各関節を指定していきます.
sc2-image
再投影誤差最小化をして関節角度を求めている様子です.
ss1
以下が得られた姿勢ですが, 奥行きの推定がうまくいってないのがわかります. やっぱり奥行きの推定は難しいですね.
sc2-model

sc2-model2

今回の実験ではあまり上手くいきませんでしたが, いくつか改善の余地はあると思っています. たとえば, 骨長さは身長が決まればだいたいわかるので, いくつか人体モデルを用意して骨長さは既知として解く, 踊ってみた動画は撮影中カメラが固定されていると考えられるので各フレームスケールファクタをすべて同じと仮定することで未知数を減らす, などです.

カテゴリー: Computer Vision, OpenCV, OpenGL | コメントをどうぞ

萌えキャラ顔画像分類システムを試作してみた

最近NBNNも学んだことだし、顔という似通っているものを分類するというのはチャレンジングな内容なので、挑戦してみようと思ったと。

とりあえずサクッと実験したかったので、OpenCVを使いつつNBNNを実装して、特徴点検出には高速な Star Detector を、デスクリプタはSIFTと色ヒストグラムを用いました。
クラスを形成するデスクリプタの生成に用いた画像は約15000件で176クラスあります。
下の画像がデスクリプタを登録した画像の一例です。顔の向きは定まっておらず、背景も単色とは限りません。

ちなみに、画像はある方が学習用に用意されていたものを拝借させていただいきました。感謝。

システムの構成は下のようになっています。

クライアントは、今回Chrome Extensionsで作成しましたが、WebAPIがあるのでなんでもいいです。

2000枚程度の画像で実験した結果、分類率は67%程度でした。まずまずの結果でしょうか。

現在、Androidアプリ版クライアントも作成してます。が、完成はいつになることやら・・・。
理想としては、萌えキャラ限定のエデンシステムを作りたいですね(なんの役に立つかはおいといて)。WebAPIも公開したいなと思っています。が、これもいつになることやら・・・。

実際に動作している動画を載せておきます。興味のあるかたはご覧ください。

【ニコニコ動画】萌えキャラ顔画像分類システムを試作してみた

2012/11/17追記:
とりあえず分類部分だけ公開しました。
http://face.moekyun.net/welcome/index

カテゴリー: Computer Vision, Machine Learning, OpenCV, Video | コメントをどうぞ