opencvr: OpenCV Rubyバインディング
OpenCVのRubyバインディングであるopencvrの0.2をリリースしました。
opencvrとは何か、なぜ作ったか、という話については個人ブログに書きましたが(リンク1、リンク2)、改めてここで紹介記事を書かせてもらいます。
使い方
使い方はGithubのWikiに書きましたが、以下に例を挙げます。gemはまだ作っていないので自分でビルドする必要があります。
requireとパス設定
opencvrを使うスクリプトの書き出しは以下のようになります。
#/usr/bin/env ruby
$:.unshift(__dir__)
require 'numo/narray'
require 'cv2'
cv2
の他にnumo/narray
も明示的にrequireする必要があります。また、cv2
をrequireするとき、cv2.so
があるディレクトリがRubyのライブラリパスに含まれている必要があります。上の例はこのRubyファイルがcv2.so
と同じディレクトリにあることを想定しており、$:
(ライブラリパスのリスト)に__dir__
(そのRubyファイルがあるディレクトリ)をunshift()
でリストの先頭に追加しています。この部分はファイルの場所に合わせて変更してください。
画像の読み込み/作成
何をするにもまずは画像を読み込む or 作成しなければ話が始まりません。画像ファイルを読むにはCV2::imread()
を使います。
img = CV2::imread("input.jp", CV2::IMREAD_COLOR)
pp img.shape
600x400 3チャンネルカラー画像なら[400, 600, 3]
などのように表示されるはずです。第2引数にCV2::IMREAD_GRAYSCALE
を指定するとグレースケールとして読み込み、shapeは[400, 600]
になります。なおデフォルトでCV2::IMREAD_COLOR
になるのでカラー画像として読む場合は第2引数は省略することもできます。
読み込んだ画像はNumo::NArrayになります。pp img
とすればNumo::UInt8
になっているはずです。OpenCVのPythonバインディング版ではPythonのデファクトスタンダードの行列ライブラリであるnumpy.ndarrayになっていますが、これに相当するものしてNumo::NArrayを使っています。
空の画像を作る場合はNumo::UInt8のインスタンスを作成することで実現できます。横600px, 縦400pxカラー画像を作る場合は以下のようにします。すべて0で初期化されているので真っ黒の画像です。
img = Numo::UInt8.zeros(400, 600, 3)
画像の保存
CV2::imwrite()
で画像を保存できます。
img = Numo::UInt8.zeros(400, 600, 3)
CV2::circle(img, [200, 200], 50, [255, 0, 0], thickness: 3, lineType: CV2::LINE_AA)
CV2::imwrite("out.jpg", img)
画像の表示
作成した画像を確認するだけなら保存しなくてもCV2::imshow()
で画面に表示できます。何かキーを押すと終了します。
img = Numo::UInt8.zeros(400, 600, 3)
CV2::circle(img, [200, 200], 50, [255, 0, 0], thickness: 3, lineType: CV2::LINE_AA)
CV2::imshow("Test", img)
CV2::waitKey(0)
CV2::destroyAllWindows()
Macで試したところ表示されたウィンドウにフォーカスできず、killしないと終了できなくなりました。Python版でも同様で、原因は掴めていません。
API
- クラス・メソッド・モジュール名
- クラス・メソッド名はC++, Pythonと同じ。
- トップのモジュールは
CV2
、それ以外は1文字目を大文字にしたもの。- 例えばC++の
cv.xphoto.oilPainting()
はRubyではCV2::Xphoto::oilPainting()
。
- 例えばC++の
- 引数・戻り値
- C++でオプショナルになっている引数はRubyでも省略可能。
- オプショナルの引数はキーワード引数も使用可能。キーワードはC++の仮引数と同じ。
- 必須引数はキーワードにはできない。
- 引数が出力に使われる場合(引数が非const参照など)、結果は戻り値として返る。
- 特別対応のクラス
-
cv::Mat
はNumo::NArrayになる。Python版においてnumpy.ndarrayが使われているのと同様。 -
cv::Size
,cv::Point
,cv::Rect
などはArray
になる。- 例えば
cv::Size
は数値2つを持つArray
、cv::Rect
は数値4つを持つArray
。
- 例えば
-
例1: cv2::circle
Rubyでは CV2::circle(img, [200, 200], 50, [255, 0, 0])
などのようになります。
-
img
:imread()
で読んだ、あるいはNumoのAPIで作成した画像バッファ -
center
: 中心座標。Point
はRubyでは2要素を持つArray -
radius
: 半径 -
color
: 色。Scalar
はこの場合3要素(BGR)を持つArray
残りの引数はオプショナルなので追加することもできます。また、オプショナルの引数はキーワードで指定することもできます。
# thicknessとlineTypeを指定
CV2::circle(img, [200, 200], 50, [255, 0, 0], 1, CV2::LINE_AA)
# lineTypeのみをキーワードで指定
CV2::circle(img, [200, 200], 50, [255, 0, 0], 3, lineType: CV2::LINE_AA)
例2: cv2::clipLine
cv::clipLine()
は、C++では3つの引数imgSize
, pt1
, pt2
を受け取りbool
を返しますが、pt1
, pt2
は非const参照で出力にも使われます。そのためPython/Rubyでは戻り値が3つになります。
対応機能
まだすべてのクラス・メソッドに対応しているわけではありません。対応状況は[GithubリポジトリのWiki]に書いてあるので参照してください。
サンプル
輪郭抽出
画像内のオブジェクトの輪郭を抽出し、それを元画像の上に描画します。上が元画像、下が輪郭を描画したもの。
#!/usr/bin/env ruby
$:.unshift __dir__
require 'numo/narray'
require 'cv2'
img_c = CV2.imread("2d-shapes.png")
img_c2 = img_c.clone()
img_g = CV2.cvtColor(img_c2, CV2::COLOR_BGR2GRAY)
contours, hierarchy = CV2.findContours(img_g, CV2::RETR_TREE, CV2::CHAIN_APPROX_SIMPLE)
CV2::drawContours(img_c2, contours, -1, [0,0,255], 2)
img = CV2::vconcat([img_c, img_c2])
CV2::imwrite("out.png", img)
ヒストグラム平坦化
画像の明るさに偏りがある場合、平坦化すると見やすくなります。これを行うのがCV2::equalizeHist()
ですが、より良いアルゴリズムとしてCLAHE (Contrast Limited Adaptive Histogram Equalization) というのがあり、これもOpenCVに含まれています。
#!/usr/bin/env ruby
$:.unshift __dir__
require 'numo/narray'
require 'cv2'
img1 = CV2::imread("clahe-sample.jpg", CV2::IMREAD_GRAYSCALE)
img2 = CV2.equalizeHist(img1)
clahe = CV2::createCLAHE(clipLimit=2.0, tileGridSize=[8,8])
img3 = clahe.apply(img1)
img = CV2::vconcat([img1, img2, img3])
CV2::imwrite("out.jpg", img)
上から順に元画像、CV2::equalizeHist()
の結果、CLAHEの結果。元画像は暗くて見えない場所や明るすぎて白飛びしている場所がある。equalizeHist()
の結果全体に見やすくなったが下にある彫像(元から明るかった)が明るくなりすぎて白飛びしてしまっている。CLAHEはそれが改善されている。
線分検出(LineSegmentDetector)
#!/usr/bin/env ruby
$:.unshift __dir__
require 'numo/narray'
require 'cv2'
img_c = CV2::imread("sample3.jpg", CV2::IMREAD_COLOR); if img_c == nil then puts "read error"; end
img_g = CV2::cvtColor(img_c, CV2::COLOR_BGR2GRAY)
lsd = CV2::createLineSegmentDetector(CV2::LSD_REFINE_STD)
lines, width, prec, nfa = lsd.detect(img_g)
line_img = Numo::UInt8.zeros(img_g.shape[0], img_g.shape[1], 3)
lsd.drawSegments(line_img, lines)
img = CV2::vconcat([img_c, line_img])
CV2::imshow("Test", img); CV2::waitKey(0); CV2::destroyAllWindows()
QRコードの検出と読み取り
OpenCVにはQRコードの読み取り機能もあります。ただ、これを使うにはOpenCV本体のビルド時にQUIRCの使用を有効化しておく必要があります。Ubuntu-22.04標準パッケージのOpenCVでは有効になっていないため使えません。opencvrを自前でビルドしたOpenCVと組み合わせることも可能で、以下はその結果ですが、方法はそのうち書きます。
入力画像
作成にはこちらを使わせてもらいました。
#!/usr/bin/env ruby
$:.unshift __dir__
require 'numo/narray'
require 'cv2'
img = CV2::imread("../images/qrcode1.png")
qcd = CV2::QRCodeDetector.new()
ret, decinfo, pts, straight_qrcode = qcd.detectAndDecodeMulti(img)
decinfo.each_with_index{|s, i|
puts "decinfo[#{i}]: \"#{s}\""
}
結果
decinfo[0]: "Hello"
decinfo[1]: "opencvr sample"
終わりに
Numo::NArrayと組み合わせて使えるOpenCV Rubyバインディングであるopencvrを紹介しました。まだ機能・品質とも足りていないのですぐに使おうという人は少ないかもしれません。今後使ってみたいと思ったらこの記事のいいねボタンやGithubリポジトリのスターを押してくれたりすると作業が捗るかもしれません。
Discussion