🐕

opencvr: OpenCV Rubyバインディング

2023/03/27に公開

OpenCVのRubyバインディングであるopencvrの0.2をリリースしました。

https://github.com/wagavulin/opencvr

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++でオプショナルになっている引数はRubyでも省略可能。
    • オプショナルの引数はキーワード引数も使用可能。キーワードはC++の仮引数と同じ。
    • 必須引数はキーワードにはできない。
    • 引数が出力に使われる場合(引数が非const参照など)、結果は戻り値として返る。
  • 特別対応のクラス
    • cv::MatはNumo::NArrayになる。Python版においてnumpy.ndarrayが使われているのと同様。
    • cv::Size, cv::Point, cv::RectなどはArrayになる。
      • 例えばcv::Sizeは数値2つを持つArraycv::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