OpenCVの新しいRubyバインディングであるopencvr-0.1をリリースしました。とは言っても現状はまだ足りない機能が多く実用的とは言い難い状態なので、どちらかと言うと「OpenCVのRubyバインディングの開発を始めました」というアナウンスという位置づけです。
https://github.com/wagavulin/opencvr
gemはまだ作っていないのでコマンド一発でインストールとはいかないですが、apt/brewでインストールしたOpenCVを使ってビルドできるようにはなっています。Ubuntu-20.04かmacOSの環境なら試すのはそんなに難しくないはずなので、気が向いたら試してもらえればと思います。方法はREADME.mdのHow to installを参照してください。
サンプルとスクリーンショット
元画像
Drawing
画像に矩形や直線などの図形や文字を書き込みます。
#!/usr/bin/env ruby require 'cv2' img = CV2::imread("input.jpg") CV2::putText(img, "Hello OpenCV", [50, 50], CV2::FONT_HERSHEY_DUPLEX, 1.0, [0, 0, 0], lineType: CV2::LINE_AA) CV2::imwrite(__dir__ + "/out-drawing.jpg", img)
油絵風
#!/usr/bin/env ruby require 'cv2' img = CV2::imread("input.jpg") out = CV2::Xphoto.oilPainting(img, 2, 5, CV2::COLOR_BGR2Lab) CV2.imwrite(__dir__ + "/out-oil.jpg", out)
鉛筆画風
白黒
色付き
#!/usr/bin/env ruby require 'cv2' img = CV2::imread("input.jpg") out1, out2 = CV2.pencilSketch(img, sigma_s: 60, sigma_r: 0.07, shade_factor: 0.05) CV2.imwrite(__dir__ + "/out-pencil1.jpg", out1) CV2.imwrite(__dir__ + "/out-pencil2.jpg", out2)
使い方
Python版とだいたい同じです。
// C++ cv::Mat img = cv::imread("input.jpg", cv::IMREAD_COLOR);
# Python img = cv2.imead("input.jpg", cv2.IMREAD_COLOR)
# Ruby img = CV2::imread("input.jpg", CV2::IMREAD_COLOR)
- C++ APIでオプショナルになっている引数はRubyでも省略可能です。
- オプショナルな引数はキーワード引数も使えます。キーワード名はC++ APIの仮引数名と同じです。
- 必須引数はキーワード引数にはできません。
CV2
以外の名前空間名は、Rubyでは最初の文字のみ大文字にしたものになっています。- C++の
cv.xphoto.oilPainting()
はCV2::Xphoto::oilPainting()
です。
- C++の
cv::Size
,cv::Point
,cv::Rect
などはArray
になります。- 例えば
cv::Size
は数値2つを持つArray
です。
- 例えば
- 引数が出力に使われる場合(引数が非const参照など)、結果は戻り値として帰ります。
OpenCVのAPI ReferenceにはPython APIも載っているで詳しくはそちらを参考にしてください。例えば以下のcv::clipLine()
は、C++では3つの引数 imgSize
, pt1
, pt2
を受け取りbool
を返しますが、見ての通りpt1
, pt2
は非const参照で出力にも使われます。従ってPython/Ruby APIでは戻り値が3つになります。
なぜ作ったか
OpenCVは非常に有名な画像処理・コンピュータビジョンのライブラリで、C++で書かれてます。Pythonバインディングは公式に提供されていますがRubyバインディングについてはgemがいくつかありますが、決定版と言えるものはない感じです。古いバージョンについてはruby-opencvがよく使われていましたが、最新のOpenCV-4系では使えません。OpenCV-2.XまではC++/C両方のAPIが用意されており、ruby-opencvはC APIを使ってバインドされていましたが、4.0からはC APIが廃止されてしまったためです。
red-opencv, opencv-glib
新しいOpenCVに対応したRubyバインディングとして、red-opencvとopencv-glibがあります(2つを組み合わせて使います)。まずopencv-glibはOpenCVのC++ APIのGObjectバインディングを提供します。GObjectの詳細はここでは省きますが、GObjectからはRubyだけでなくPerl, Javaなどといった様々なプログラミング言語のバインディングを自動生成することができます。これを使ってRubyバインディングを提供するのがred-opencvです。
このopencv-glibについては私も以前に開発に参加して私のコードも少し入っていますが、手が止まってしまいました。理由は大きく2つあります。
前者の問題についてはglibに対する私の知識不足もあるので、実はたいした問題ではないかもしれません。しかし後者は由々しき問題です。試しにPython APIでバインドされている関数・enumをざっと数えたところ4000近くありました。
- クラスに属さないグローバルな関数: 892
- クラスに属する関数: 2925
- enum型: 195
手作業でやると毎日休まず1日10個作っても10年以上かかるわけで、この数字を見たときに心が折れました。
ちなみに別の方法としてPyCallを使うというのがあります。私は試してはいませんが、Pythonバインディングが既に存在する以上、PyCallを使えばRubyからも呼べるはずです。ただそれを言ってしまうと話が終わってしまうのでそこはスルーして進めます。とにかくRubyから直接呼びたいのです(あんまり深い理由はないですが)。
Pythonバインディングとopencvr
では公式でサポートされているPythonバインディングはどうしているかというと、バインディングコードを自動生成しています。OpenCVのC++ APIのヘッダファイルからインターフェース情報を読み取り、それを基にバインディングコードを生成します。具体的にはhdr_parser.py
というスクリプトでヘッダファイルの情報を読み取り、それを基にgen2.py
がPythonのバインディングコードを生成します(もっと細かく言うと、自動生成コード以外ににいくつかの.cpp, .hppを使っています)。
この仕組みを真似すればRubyバインディングも自動生成できるはず、という発想で作ったのがopencvrです。hdr_parser.py
はそのまま流用し、gen2.py
を独自のものに置き換えています。Python用のgen2.py
やその他.cppファイルなど合わせて4000行くらいあり、それのRuby版を作るわけですが、4000個の関数のバインディングコードを書くよりはかなり作業量は減るはずです。
実装状況
こうして始めたopencvrですが、実装状況はまだ未熟です。ざっくり言うとバインドできているのは以下の条件を満たすもののみです。
- クラスに属さないグローバル関数である
- 引数・戻り値とも
int
,float
などの基本型もしくはcv::Size
,cv::Rect
など、Ruby側でArray
にバインドされているものである。std::vector<int>
など、これらを要素に持つstd::vector
はサポート。
- クラスは現状未サポートだが、
cv::Mat
のみは使用可能(というかこれがないと始まらない)。ただし使えるメソッドはcols()
,rows()
,channels()
,at()
のみ。
グローバル関数のみ、と聞くとほとんど何もできないじゃないかと思われそうですが、OpenCVのAPIはグローバル関数になっているものが結構あるのでそれなりに使えます。詳しいバインド状況はWikiページにあります。
またOpenCVの中心的なクラスであるcv::Mat
のメソッドがほとんど対応していないのは理由があって、次に書きます。
cv::Matと数値計算ライブラリ
OpenCVでは画像データなど多くのデータを表すのにcv::Mat
クラスを使います(APIリファレンス上ではcv::InputArray
, cv::OutputArray
, cv::InputOutputArray
になっているところ)。Python APIではこのcv::Mat
クラスに対応するPythonのクラスを作るのではなく、Numpyのndarray
クラスを使っています。例えば画像ファイルを読み込むcv::imread()
関数は、C++ APIではcv::Mat
インスタンスを返しますが、Python APIではndarray
を返します。Pythonの画像処理・機械学習・データサイエンス系のライブラリの多くはndarray
を使っているので、それらのライブラリと容易に連携させることができるわけです。
import cv2 img = cv2.imread("input.jpg") print(img.__class__) # => <class 'numpy.ndarray'>
これをどうやって実現しているかというと、cv::Mat
のAllocatorという機能を使っています。これはcv::Mat
が内部で使うメモリを確保するときに使用する関数を指定するもので、これとNumpyのC APIを組み合わせて実現します。例えばcv::imread()
を読んだとき、以下のことをやります。
cv::imread()
の戻り値 (m
) とは別に空のcv::Mat
インスタンス (temp
) を作る。temp
のAllocatorに独自のAllocatorをセットする。m
をtemp
にコピーする。- このとき独自のAllocatorが使われる。その中ではNumpyの
PyArray_SimpleNew()
を使ってメモリを確保する。 - 確保した領域へのポインタが
cv::Mat
インスタンス内にセットされる。
- このとき独自のAllocatorが使われる。その中ではNumpyの
出来上がったtemp
とPython側に返すポインタは以下のようになっています。
現状のopencvrはこのようなことはやってはおらず、Ruby側にもMat
クラスを定義するようになっています。Pythonと同様のことをするならNumo::NArrayが候補になると思いますが、そもそも技術的に可能かどうかの検討もしていないので今後どうするかは未定です。cv::Mat
クラスのバインディングが手抜きなのはこれが理由です。
なお検討していない理由は時間の都合もさることながら、Numo::NArrayのC APIの使い方がさっぱり分からないというのが原因なので、分かりやすいドキュメントなどあったら教えてほしいです。
まとめ
- OpenCVのRubyバインディング作ってます
- 現状は未対応の部分がたくさんあります
- Python版の仕組みを真似しているので、Python版の同等のことが現実的な工数できるはずです
- Numpyみたいなものを使うかは未定です
と、ここまで書いたところで改めてgemを探すとropencvというgemがC++ヘッダから自動的にバインドするRubyインターフェースを提供しているっぽいのを見つけました。opencvr-0.1.0を作るまでにそれなりに時間使ったのですが、無駄だったんですかねぇ。