non vorrei lavorare

昔はおもにプログラミングやガジェット系、今は?

Julia言語からmruby-webcamを使ってWebカメラの画像を取得した

こんにちは、連休前から沖縄の離島に出かけていた奥さんと子供たち。長男2日目の船では船酔いで吐いたとの連絡があったものの、その後は元気に次男ともども沖縄の離島の滞在を楽しんだようです。@kjunichiです。

背景

Goの書籍を書かれたり、SD誌に連載を書かれたりされてる@mattn_jpさんから、なんとmruby-webcamへプルリクエストを頂きました!

そんなわけで平成から令和にかわるこの10連休にmruby-webcamJulia言語から利用してみることにしました

mattnさんといえばWindowsなので、リスペクトの意味合いも込めて、以降はWindowsでの話。 Windowsの場合、Juliaは未だにmingwでビルドされており、mrubyはVC++で自分はビルドしているので、 このあたりの混ざり具合も気になるところだが、64ビット化されてからは、stdcallやcdeclの違いもあまり意識することが なくなり、Windowsでも割とほかのプラットフォーム並みに利用できる感覚を覚えつつありますが。

みんなのGo言語[現場で使える実践テクニック]

みんなのGo言語[現場で使える実践テクニック]

ソフトウェアデザイン 2019年5月号

ソフトウェアデザイン 2019年5月号

  • 作者: 上田拓也,mattn,渋川よしき,鹿志村秀昭,曽利雅樹,舟窪恵一,向山優,安井正明,渡部敏雄,根本祐介,座間政紀,吉田英二,安藤幸央,結城浩,武内覚,宮原徹,平林純,くつなりょうすけ,中島明日香,上川慶,職業「戸倉彩」,中村壮一,田代勝也,山田泰宏,上田隆一,嶋是一,小飼弾,青田直大,あわしろいくや,中島雅弘,横田結菜,西谷友彬,大塚和彦,後藤大地,杉山貴章,Software Design編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2019/04/18
  • メディア: 雑誌
  • この商品を含むブログを見る

mrubyをビルドできる環境の作成

abrakatabura.hatenablog.com

などをみて、適当に作成してください。

mrubyにmruby-webcamを組み込む

mruby配下のbuild_config.rbに以下を追加する。

conf.gem :github => 'kjunichi/mruby-webcam'
conf.gem :github => 'mattn/mruby-sharedlib'
conf.gem :github => 'mattn/mruby-base64'

mattn/mruby-sharedlib、mattn/mruby-base64の追加理由は後述。

mruby.dllを作る

基本的には、mruby-sharedlibがmruby.dllを作ってくれる。

しかし、現在は一部のシンボルが廃止されたり、Cのマクロ定義に変更されてしまっているので、

以下のようにDLL定義ファイルのmruby.defを変更する必要がある。

Juliaからmruby.dllを利用する

Cで利用するには、mruby.hなど必要なヘッダファイルをインクルードすることで、必要な構造体等の定義が 手に入るが、JuliaのようなC以外の処理系から利用する場合、必要な定義を自前で作成する必要がある。

mrb_open

mrb_openでruby仮想マシンを取得する。Cではmrb_stateのポインタ型が受け取れるが、Juliaの場合 自前で用意する、といってもJulia側からmrb_stateの内部構造にはアクセスする必要がないので、 先頭アドレスを保持できる入れ物を用意するイメージ。

以下のようにUInt型のポインタを指定。

mrb = ccall( (:mrb_open, "mruby"), Ptr{UInt}, ())

mrb_load_string

以降は、普通にRubyをJuliaから実行するだけ。

注意点はローカル変数がトップレベルでの実行のためか、mirbのように使えないので、 $を変数の前につけて明示的にグローバル変数として扱うことで、これに対処している。 また、「$」はJuliaの予約語に含まれているので、「\」でエスケープして使う。

あと、macOSで同様の処理を行った際には問題になりませんでしたが、Windowsの場合、 Cの関数を呼び出した後に返り値をちゃんと受け取らないと落ちました。

なので、mrb_load_stringの返り値のmrb_value型を用意する必要があります。 これ、ポインタではなく構造体の実体なので、ちょっと悩みましたが、

struct M
    A::NTuple{16,Cuchar}
end

と16バイトの入れ物を用意すれば、この問題を解決できます。こちらも、Julia側からmrb_value型の 中に触れなければ、入れ物を用意する事でやり過ごすことができるのです。

ccall( (:mrb_load_string, "mruby"),M,(Ptr{UInt},Cstring),mrb,"\$cam=Webcam.new")
ccall( (:mrb_load_string, "mruby"),M,(Ptr{UInt},Cstring),mrb,"\$cam.capture{|img| \$img2=img}")
ccall( (:mrb_load_string, "mruby"),M,(Ptr{UInt},Cstring),mrb,"\$cam.snap")

mrb_str_to_cstr

JuliaからRubyを実行するだけなら、これまでの方法でとりあえずOKですが、Rubyで実行した結果を Juliaから利用したい場合、このままだと、ファイルベースでのやり取りしか手段がない

mrubyでは、この問題に対処するために、いくつか方法が用意されています。 しかし、C以外から利用することは多分あまり考慮されておらず、バイナリの画像データをそのまま 持ってくる手段が見当たりませんでした。

今回は、mruby-webcamで撮影したJPEG形式の画像データをBase64エンコードして文字列化して、 Julia側からmrubyで用意されているmrb_str_to_cstrを使って文字列として取り出すことにします。

このために、前述のbuid_config.rbにmruby-base64のmrbgemを加えたのでした。

obj = ccall( (:mrb_load_string, "mruby"),M,(Ptr{UInt},Cstring),mrb,"Base64::encode(\$img2)")
base64jpeg = ccall( (:mrb_str_to_cstr, "mruby"),Cstring,(Ptr{UInt},M),mrb,obj)

以上で、Julia側からmruby-webcamを利用してカメラで撮影して、画像データを取得できました。

Julia側で画像データを扱う

Base64でコード

JuliaでもBase64エンコード/デコードは当然用意されている。 これを使ってバイナリに戻す。

using Base64

jpeg=base64decode(unsafe_string(base64jpeg));

メモリ上のJPEGデータを扱う

JuliaではImages.jlが画像ファイルを扱う際の標準的なパッケージの模様。 しかし、今回、ファイルを介さずとも、ダイレクトでメモリ空間上にJPEG形式で画像データを取得することが できており、ファイルを介さず画像データを扱いたいところ。

ちょっと調べたら、この問題の解決方法がありました。Images.jlがWindowsLinuxで内部的に利用しているImageMagick.jlで blobreadなる関数があり、これが利用できました。

using Images
using ImageMagick

img = readblob(jpeg)

成果物

gist.github.com

動画版

www.youtube.com

まとめ

せっかくmattnさんが作成してくださったeachメソッドは自分の力不足でJuliaからは利用するには至りませんでしたが、 何とか、mruby-webcamをJuliaから利用することだけはできました。

今回の作業で、一般にCのAPIを持たない、C++APIのみを持つライブラリをmrubyのgemであるmrbgemにすることで、 FFIが使える別の言語処理系からも利用することが可能であることが実証できました!

C++オンリーなAPIしかないライブラリはmrbgem化すると別の言語処理系からも利用できて一粒で2度以上美味しいということです。

参考資料

関連記事

Webカメラつながり

7年前の記事