OpenCVをRubyMotionから使う方法
RubyMotionからOpenCVを使えるようにするの(MotionCV)を今年ずっとやってましたのでAdvent Calendarではその解説を行います。
MotionCVの仕組み
RubyMotionから使えるようにOpenCVをビルドするのはこのエントリーにも書いているのですが、OpenCVはC++で書かれているためiOS用にビルドされたライブラリをそのまま使えません。
OpenCVのiOS用ビルド環境を使いながら、RubyMotionからC++が見えないようにObjective-Cの薄いラッパーを作りそれを通して使います。
ビルド方法
ラッパープログラムはOpenCVのmodulesの一部として配備して、RakefileからOpenCVのiOS用ビルドスクリプトの中で一緒にビルドします。
具体的には私のOpenCVのForkのmotioncvブランチをcloneしてrakeします。
git clone https://github.com/iwazer/opencv.git
cd opencv
rake
ビルドが終わるとgem/framework/opencv2.framework
が生成され、これがそのままRubyMotionプロジェクトの3rd Party Frameworkとして使用できます。
Objective-Cによるラッパー
OpenCVは非常に多くの機能を持っていますが、現在のMotionCVはcore、imgprocのごく一部のみ実装しています。
coreは画像などを表現する基本的な構造体やクラス、それに対してビット操作したり描画したりするための基本的な機能の集合です。
imgprocは画像処理のためのビルトインフィルターやより複雑な変換、フィルターを自分で作るための機能の集合です。
MotionCVではmodules/motion
配下でObjective-Cのヘッダファイルと実装ファイルとしてラップします。
$ tree modules/motion
modules/motion
├── CMakeLists.txt
├── include
│ └── opencv2
│ └── motion
│ ├── MotionCV_capi.h
│ ├── MotionCV_core.h
│ ├── MotionCV_helper.h
│ ├── MotionCV_imgproc.h
│ ├── MotionMat.h
│ └── core_base.h
└── src
├── MotionCV_capi.mm
├── MotionCV_core.mm
├── MotionCV_helper.mm
├── MotionCV_imgproc.mm
├── MotionMat.mm
└── MotionMat_p.h
OpenCVの実装ファイルは非常に細かく分かれていますが、MotionCVではC++リファレンスの単位でファイルを分割しました。これが渡しには分かりやすかったので。
それぞれのソースファイルについて説明します。
MotionMat.{h,mm}、MotionMat_p.h
coreで定義されている画像のピクセルを保持するcv::MatをMotionMatというObjective-Cクラスでラップしています。
RubyMotionからは見えないが、ラッパーの実装コードからは見たいインスタンス変数やメソッドをMotionMat_p.h
に分離しています。
MotionMat#mat
というインスタンスメソッドが、本来のcv::Mat
インスタンスを返すのですがRubyMotionからは見えません。逆に見えるとコンパイルエラーになります。
ちなみにヘッダファイルのinclude/opencv2/motion
という格納場所はビルドルールでフレームワークの公開ヘッダファイルとなっています。
C++とObjective-C両方のオブジェクトを扱うので、実装ファイル名の拡張子は.mm
です。
MotionMatクラスは内部にcv::Matインスタンスを持ち、生成とcv::Matのメソッドへのプロキシの役割をします。
ただし、MotionMatにプロキシメソッドを追加するのは結構難しいです。例えばcopyToという自分の内部状態を、引数で渡されたcv::Matにコピーするメソッドがあるのですが、今のところメモリ管理がうまくいかずMotionMatのメソッドとしては使えるようにできてません(-ω-)
MotionCV_core.{h,mm}
coreに定義されているcv::Matを操作する関数群をラップしています。
cv::circle
を例にとると
+ (void)circle:(MotionMat *)mat
center:(CGPoint)center
radius:(int)radius
bgr:(int *)bgr
thickness:(int)thickness
lineType:(int)lineType
shift:(int)shift;
+ (void)circle:(MotionMat *)mat
center:(CGPoint)center
radius:(int)radius
bgr:(int *)bgr
thickness:(int)thickness
lineType:(int)lineType
shift:(int)shift
{
cv::circle([mat mat],
cv::Point(center.x, center.y), radius,
cv::Scalar(bgr[0], bgr[1], bgr[2]),
thickness,
lineType,
shift);
}
このようにcv::circle
にcv::Mat
を渡し、パラメータのつじつまを合わせているだけです。
centerのような位置指定はCGPointを使い、Ruby側で配列でも指定できるようにしてあります。
また、bgrは色の指定なのですが、Ruby側からはもう1段ラッパーを入れて、これも配列で渡せるようにしてあります。
試行錯誤中なのでもっと良い方法があれば変えるかもしれませんが
Cのプリミティブ型や構造体、Objective-CのオブジェクトはRubyMotionがうまくやってくれるのでそのまま使って問題ありません。
MotionCV_imgproc.{h,mm}
については、これとほぼ同じなので詳しい説明は割愛します。
MotionCV_helper.{h,mm}
OpenCVとRubyMotionの橋渡しをするための関数を定義しています。現在、UIImageとMotionMatを相互に変換する関数があります。
@interface MotionCV_helper : NSObject
+ (MotionMat *)MotionMatFromUIImage:(UIImage *)image;
+ (UIImage *)UIImageFromMotionMat:(MotionMat *)mat;
@end
OpenCVの機能ではないがObjective-Cでの実装が必要な機能はここに定義していくつもりです。
MotionCV_capi.{h,mm}
@watson1978さんがC APIをRubyMotionから使えるようにラップしたものを入れています。まだ巷にはC APIのサンプルも多いので、こちらを拡張するのも有用かもしれません。
Rubyによるラッパー
RubyMotionはObjective-Cのメソッドをそのまま呼び出せるのですが、もう少し使いやすく、またC++ APIを使ったOpenCVのサンプルをRubyに置き換えやすくしたいと考えRubyのラッパーレイアも作ってあります。
$ tree gem/lib/project/
gem/lib/project/
├── cv
│ ├── capi.rb
│ ├── core.rb
│ ├── helper.rb
│ └── imgproc.rb
├── motion_mat.rb
└── motioncv.rb
OpenCVのC++ APIでは、cv::Mat
を生成して、cv::method(srcMat, dstMat, parameters...)
という操作系メソッドを呼ぶことによっていろいろな事ができるようになっています。
(ビルトインでない機能は自分でピクセル操作しないといけませんが…)
できるだけ、その表記法に近づけるようにCvモジュールのクラスメソッドとしてラップしてあります。
たとえば、先ほどのcv::circle
は
def circle mat, center, radius, rgb, thickness, lineType=8, shift=0
MotionCV.circle(mat, center:center.map(&:to_f), radius:radius.to_i,
bgr:rgb2bgr(rgb),
thickness:thickness, lineType:lineType, shift:shift)
end
このようになっています。実際にこれを使うと
Cv::circle(mat, [100,100], 100, [255,0,0])
のように書けて、Objective-Cのメソッドを直接呼び出すよりC++のサンプルを参考にし易いと思います。
RubyMotionから使う
RubyMotionのiOSテンプレートでプロジェクトを作ります。先ほどのMotionCVとは別のディレクトリの方が良いです。
motion create try-motioncv
cd try-motioncv
frameworkへのシンボリックリンクをvendorディレクトリに作成します。
mkdir vendor
ln -s /path/to/opencv/gem vendor/opencv
Gemfileにmotioncvを追加してbundleを実行。
source 'https://rubygems.org'
gem "rake"
gem "motioncv", :path => "/path/to/opencv"
3rd Party Frameworkの設定などはgemの中で自動的にされますので、これでMotionCVが呼べるようになります。
まとめ
実際に動くサンプルを作りながら、MotionCVに足りないメソッドを追加するのもやりたかったのですが、長くなってしまったので、別のエントリーにしたいと思います。
とりあえず実際に動く例はRubyMotionとOpenCVでマンガ風フィルタを参照下さい。
最後に、現状のTODOをまとめて、来年のもくもく会やRubyMotionTokyoでのネタを確認しときますw
- arch=arm64対応(本家のmasterは対応されてるっぽいけどビルドにコケるので様子見中…)
- cv::MatのメソッドをMotionMatで単にプロキシするとうまく動かない(ex. copyTo)のがあるのを何とかする
- メモリマネージメントの確認。リークしてる気がする…
- OpenCV中のマクロや定数でRubyMotionから見えないものを何とかしたい
- ビデオ系の機能もやってみたい
- |ω・)<テストがないお
- 画像処理の経験が乏しくつらいので画像処理とOpenCV自体の勉強 orz
- OSXでもRubyMotionから使えるようにしたい気がする