Edited at

ScalaでOpenCVを使って画像処理

More than 5 years have passed since last update.


OpenCV meets Scala

OpenCV meets Scala

OpenCV 2.4.4から正式にJava APIをAndroidプラットフォーム以外でも使えるようになりました。つまりJVM(Java Virtual Machine)上で動く言語ならどんな言語からでもOpenCVが使えるということですね。

* OpenCV now supports desktop Java

、ということで今回はJVM上で動作するScalaからOpenCVを使ってみます。


環境


  • CentOS 5.8 (x86_64, 仮想2コア, 1GB RAM)

  • Scala 2.10.0 (sbt 0.12)

  • Java 1.6.0 (java-1.6.0-openjdk.x86_64)

  • OpenCV 2.4.4

Java APIを使う際にAndroidプラットフォームを意識する必要はもうなくなりましたので、ここではアプリケーションサーバから使えるようにLinux機に環境構築して試しました。OpenCVのLinuxへのインストール手順については技術Wikiにメモを残していますのでそちらを参照してください。

* OpenCV - 2.x Tech Note

公式サイトではeclipseやsbtなどでのプロジェクト構築手順もスクリーンショット付きで載っているのでこちらも参考に。

* Introduction to Java Development


sbt (Simple Build Tool)

bt(Simple Build Tool)はScala/Java用のビルドツール。ライブラリの依存関係を自動で解決してくれるし、Scalaで書かれたDSLを使ってビルド設定を簡潔に記述できます。今回はEclipseのようなIDEは使わずにemacs + sbtでストレスなく開発ができました。OpenCVのインストールに成功したら、ライブラリファイル(opencv-244.jar, libopencv_java244.so)をsbtプロジェクトのlibディレクトリにコピーしておきます。sbtが自動でクラスパスを追加してくれます。


Hello OpenCV in Scala


core

まずは基本から。JNI(Java Native Interface)経由でMatの機能を使えるか試します。

import org.opencv.core.Core

import org.opencv.core.Mat
import org.opencv.core.CvType
import org.opencv.core.Scalar

object HelloOpenCV {
def main(args:Array[String]) {
println("Welcome to OpenCV " + Core.VERSION)
// 共有ライブラリ(libopencv_java244.so)をロード
System.loadLibrary(Core.NATIVE_LIBRARY_NAME)

println("Welcome to OpenCV " + Core.VERSION)
// 5行10列1チャンネルの行列(要素の型は符号無し8ビット整数)を生成
val m1 = new Mat(5, 10, CvType.CV_8UC1, new Scalar(0))
println("OpenCV Mat: " + m1)
// 2行目(インデックスは0から)を取得
val m1r1 = m1.row(1)
// 2行目の要素の値を全て1にセット
m1r1.setTo(new Scalar(1))
// 6列目(インデックスは0から)を取得
val m1c5 = m1.col(5)
// 6列目の要素の値を全て5にセット
m1c5.setTo(new Scalar(5))
println("OpenCV Mat data:\n" + m1.dump())

// 3行3列1チャンネルの行列(要素の型は32ビット浮動小数点数)を生成
val m2 = new Mat(3, 3, CvType.CV_32FC1)
// 一様分布乱数(0~25)を使って行列の要素に値をセット
Core.randu(m2, 0, 25)
println(m2.dump())
val (v1, v2, v3, v4) = (new Mat, new Mat, new Mat, new Mat)
// 行列を1行に縮小 (要素の値は合計/平均/最小/最大)
Core.reduce(m2, v1, 0, Core.REDUCE_SUM)
Core.reduce(m2, v2, 0, Core.REDUCE_AVG)
Core.reduce(m2, v3, 0, Core.REDUCE_MIN)
Core.reduce(m2, v4, 0, Core.REDUCE_MAX)
println("reduce sum: " + v1.dump())
println("reduce avg: " + v2.dump())
println("reduce min: " + v3.dump())
println("reduce max: " + v4.dump())
}
}


実行結果

$ sbt run

[info] Loading project definition from /home/ryo/workspace/src/opencv/samples/project
[info] Set current project to HelloOpenCVScala (in build file:/home/ryo/workspace/src/opencv/samples/)
[info] Compiling 1 Scala source to /home/ryo/workspace/src/opencv/samples/target/scala-2.10/classes...
[info] Running Main
Welcome to OpenCV 2.4.4.0
Mat: Mat [ 5*10*CV_8UC1, isCont=true, isSubmat=false, nativeObj=0xfe61740, dataAddr=0xfe61800 ]
Mat data:
[0, 0, 0, 0, 0, 5, 0, 0, 0, 0;
1, 1, 1, 1, 1, 5, 1, 1, 1, 1;
0, 0, 0, 0, 0, 5, 0, 0, 0, 0;
0, 0, 0, 0, 0, 5, 0, 0, 0, 0;
0, 0, 0, 0, 0, 5, 0, 0, 0, 0]
[13.25707, 4.9814796, 10.026485;
20.359627, 10.928325, 6.2197423;
19.327625, 19.052343, 7.6948619]
reduce sum: [52.944321, 34.962147, 23.94109]
reduce avg: [17.648108, 11.654049, 7.9803634]
reduce min: [13.25707, 4.9814796, 6.2197423]
reduce max: [20.359627, 19.052343, 10.026485]
[success] Total time: 11 s, completed 2013/03/17 17:43:34

行列演算はネイティブ側で行われるので高速に処理できます。


highgui, imgproc

ファイルI/Oや画像処理も試してみます。sbtプロジェクトの src/main/resources ディレクトリに画像ファイルを置いておきます。

// 画像ファイルへのパスを取得

val filePath = getClass.getResource("/src.png").getPath
// 画像ファイルの読み込み
val src = Highgui.imread(filePath)
// Cannyフィルタでエッジ検出
val edge = new Mat
Imgproc.Canny(src, edge, 80, 100)
// 半分の大きさにリサイズ
Imgproc.resize(edge, edge, new Size(), 0.5, 0.5, Imgproc.INTER_AREA)
// 画像ファイルの書き込み
Highgui.imwrite("edge.png", edge)

Highgui周りはちゃんと動くか心配してたのですが、I/O処理は特に問題ないようでした。


features2d

特徴点検出/特徴量記述、マッチング処理も試してみました。特徴量はパテントフリーで使いやすいORB特徴を使っています。

// ORB特徴点検出

val detect = (mat:Mat) => {
val keypoints = new MatOfKeyPoint
FeatureDetector.create(FeatureDetector.ORB).detect(mat, keypoints)
(mat, keypoints)
}
// ORB特徴量記述
val extract = (t:Tuple2[Mat, MatOfKeyPoint]) => {
val descriptors = new Mat
DescriptorExtractor.create(DescriptorExtractor.ORB).compute(t._1, t._2, descriptors)
(t._2, descriptors)
}
// 関数合成
val detectAndExtract = extract compose detect

// 画像ファイル読み込み
val img1 = Highgui.imread(getClass.getResource("/img1.png").getPath)
val img2 = Highgui.imread(getClass.getResource("/img2.png").getPath)

// 合成関数を適用して特徴点(キーポイント)検出 & 特徴量記述
val (keyPoints1, descriptors1) = detectAndExtract(img1)
val (keyPoints2, descriptors2) = detectAndExtract(img2)

// マッチング (ハミング距離)
val matcher = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE_HAMMINGLUT)
val matches = new MatOfDMatch
// Scalaでは match は予約語のためバッククォートで括る
matcher.`match`(descriptors1, descriptors2, matches)
// 距離が近い上位100点を選択
val filtered = matches.toArray.sortBy(_.distance).reverse.take(100)
val filteredMatches = new MatOfDMatch(filtered:_*)

// マッチング結果を画像ファイルに描画
val resultImg = new Mat
Features2d.drawMatches(img1, keyPoints1, img2, keyPoints2, filteredMatches, resultImg)
Highgui.imwrite("result.png", resultImg)

Features Matching

多少は関数型っぽくしてみましたけど、Javaの道具を使ってるので普通のJavaプログラムと変わらないように見えてしまうのは仕方ないですかね。

C++ APIで提供されている FeatureDetector/DescriptorExtractor クラスの機能はJava APIでもほぼそのまま利用できるようです。また、上記のサンプルコード内で MatOfKeyPoint や MatOfDMatch というクラスが登場していますが、どうやらJava側では std::vector を Mat のサブクラスとして扱い、JNIで渡すときに内部でコンテナ変換しているようです。ちなみにその変換部分の実装はOpenCVソースツリーの modules/java/generator/src/cpp/generators.cpp にあります。

C++コンテナ
Javaクラス

std::vector<{型名}>
org.opencv.core.MatOf{型名}

それぞれのクラスには toArray や toList メソッドが用意されていて、配列やリスト(java.util.List)のインスタンスに変換できるので運用はしやすいと思います。

以上、簡単にですがOpenCVのJava APIをScalaから使ってみました。他のJVM言語からでも簡単に使えそうなので興味ある方は是非試してみてください。