MZ Platformの機能開発記録

OpenCVの利用

OpenCVの機能をMZで利用する方法を紹介します。

OpenCVは、オープンソースのコンピュータビジョン向けライブラリで、 画像処理、特徴点抽出、物体検出などが可能です。 バージョン2.4.4からJava APIが標準的に提供されるようになったようなので、 MZ Platformから呼び出せるようにコンポーネントを開発中です。

概要

MZ Platformでは、すでに画像ファイル入出力、USBカメラからの画像取得、 基本的な画像処理の機能が存在するので、OpenCVならではの機能を主な開発対象にします。

下図に示すように、画像ファイル入出力、USBカメラからの画像取得、基本的な画像処理は それぞれ別のコンポーネントで実施し、OpenCVコンポーネントでは特徴点抽出や物体検出が 主な機能となります。また、OpenCVで扱う画像形式とMZで扱う画像形式が異なるので、 相互に変換する機能も持たせます。

クラス構成は下記のようになります。 実際にはMZ Platformのコンポーネントとして作成しますが、 ここではOpenCVに関わる部分だけ抜き出しています。 以降では、各メソッドの詳細と追加で必要な変数について説明します。

import java.awt.Image;
import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.highgui.Highgui;
import org.opencv.objdetect.CascadeClassifier;

public class OpenCVOperator {

	// DLLのロード:JavaVMの起動後に一度だけ呼び出す
	static private boolean loadLibrary();

	// 画像(Image)をMat形式に変換する
	public Mat getMatFromImage(Image image);

	// 画像(Mat)をImage形式に変換する
	public Image getImageFromMat(Mat mat);

	// ファイル指定で分類器をロードする
	public boolean loadClassifierFromFile(String filename);

	// 現在の分類器で対象を検出する
	public MatOfRect detectByClassifier(Mat image);

	// 検出結果の画像を取得する
	public Mat getMatOfDetectingResult(Mat image, MatOfRect detections, Color color);
}

ライブラリの読込

MZでOpenCVを利用するためには、OpenCVのJava API用のJARファイルをJavaのクラスパスに追加し、 OpenCVのDLLをパスに追加する必要があります。 この文章を書いた時点ではOpenCVのバージョンが2.4.6だったので、 配布されているopencv-246.jarをMZのjarsフォルダに置いてPlatformClassPath.iniにパスを追加し、 libフォルダにopencv_java246.dllを置きました。 あとは、ここで作成したコンポーネントのJARファイルを追加すれば機能が利用できます。

OpenCVの機能を利用するために、一度だけDLLをロードする必要があります。 ここでは、下記のスタティックメソッドを定義し、各メソッドの先頭で呼び出すことにしました。


// DLLのロード状態を示すスタティック変数
static private boolean loaded = false;

//
// DLLのロード:JavaVMの起動後に一度だけ呼び出す
//
static private boolean loadLibrary() {

	if (loaded) return true;

    	try {
		System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

    	} catch (Throwable e) {

		loaded = false;
		return false;
	}

	loaded = true;
	return true;
}

画像オブジェクトの変換

MZでOpenCVの機能を利用する際に重要な役割を持つのが、 Javaで通常扱われる画像オブジェクトと、OpenCV用の画像オブジェクトを相互に変換する処理です。 これによって、画像ファイル入出力やUSBカメラからの画像取得および基本的な画像処理などは 既存のコンポーネントの機能を利用し、OpenCVならではの機能に特化した実装に限定できます。


/**
 * 画像(Image)をMat形式に変換する。
 * 
 * @param image 画像(Image)
 * @return 画像(Mat)
 */
public Mat getMatFromImage(Image image) {

	loadLibrary(); // 必ず最初に呼び出す

	if (image==null || image instanceof BufferedImage == false)
		return null; // ここではBufferedImageであることが前提

	BufferedImage bimage = (BufferedImage)image;

	ByteArrayOutputStream bout = new ByteArrayOutputStream();
	try {
		// ImageIO.write(bimage, "bmp", bout); // bmpだと問題が生じる場合あり
		ImageIO.write(bimage, "png", bout); // 画像をバイト列に書き出す(2013-12-03修正)
		bout.close();

	} catch (IOException e) {
		e.printStackTrace();
		return null; // 問題があれば終了
	}
	MatOfByte mb = new MatOfByte(bout.toByteArray()); // OpenCVのバイト列作成
	Mat mat = Highgui.imdecode(mb, Highgui.CV_LOAD_IMAGE_COLOR); // Mat形式取得

	return mat;
}

/**
 * 画像(Mat)をImage形式に変換する。
 * 
 * @param mat 画像(Mat)
 * @return 画像(Image)
 */
public Image getImageFromMat(Mat mat) {

	loadLibrary(); // 必ず最初に呼び出す

	if (mat==null) return null;

	BufferedImage image = null;
	MatOfByte mb = new MatOfByte();
	Highgui.imencode(".bmp", mat, mb); // Mat形式からOpenCVのバイト列作成
	try {
		// バイト列から画像を読み込む
		image = ImageIO.read(new ByteArrayInputStream(mb.toArray()));

	} catch (IOException e) {
		e.printStackTrace();
		return null;
	}
	return image;
}

ファイル読込の注意点

OpenCVでは、Mat形式画像をファイルから読み込む機能などが当然提供されています。 しかし、実際に使ってみるとファイル読込に失敗する場合がありました。 詳細な原因をソースレベルで確認したわけではありませんが、 少なくともWindows7ではファイルパスに日本語が含まれている場合に、 JavaのAPIでは正しく読み込めないようです。

        Mat image = Highgui.imread(filename); // ファイルパスに日本語が含まれていると失敗

画像のファイル入出力については、前述の通りImage形式との相互変換を実現しているので、 MZ側で実施することでこの問題は回避できます。 一方、後述する物体検出では物体検出用のXMLファイルを読み込む必要があるので、 工夫が必要でした。

物体検出機能の呼び出し

ここでは、OpenCVならではの機能として、物体検出を取り上げます。

OpenCVでは、物体検出用の分類器を機械学習で作成する機能や、 その分類器を利用した物体検出の機能が提供されています。 物体検出用の分類器の例として、いくつかのXMLファイルも一緒に配布されているので、 ここでは分類器のXMLファイルを読み込んで物体検出を実行し、 その結果を描画するメソッドの実装方法を紹介します。


// 分類器を保持する変数
transient private CascadeClassifier detector = null;

/**
 * ファイル指定で分類器をロードする。
 * 
 * @param filename ファイルパス
 * @return ロード成否の論理値
 */
public boolean loadClassifierFromFile(String filename) {

	loadLibrary(); // 必ず最初に呼び出す

	detector = new CascadeClassifier(filename); // 分類器のロード
	if (detector.empty()) { // ロード失敗した場合
		try {
			PFFile file = new PFFile();
			file.setFile(filename);
			if (file.exists()) { // 指定のパスにファイルが存在したら
				// ファイル読込の工夫を試行

				// 一時ファイルを作成してコピー
				File tmp = File.createTempFile("tmp", ".xml");
				file.copyFile(tmp.getParent(), tmp.getName());

				// 一時ファイルから分類器をロード
				detector = new CascadeClassifier(tmp.getAbsolutePath());
			}
		} catch (Exception e) {
		}
	}

	if (detector.empty()) { // この時点で読み込まれていなければ失敗
		System.out.println("Error: Empty classifier");
		return false;
	}

	return true;
}

/**
 * 現在の分類器で対象を検出する。
 * 
 * @param image 画像(Mat)
 * @return 検出結果(MatOfRect)
 */
public MatOfRect detectByClassifier(Mat image) {

	loadLibrary(); // 必ず最初に呼び出す

	// 検出結果の格納先
	MatOfRect detections = new MatOfRect();

	if (image==null) return detections;

	if (detector==null || detector.empty()) { // 分類器がなければ終了
		System.out.println("Error: Empty classifier");
		return detections;
	}

	// 現在の分類器による対象検出
	detector.detectMultiScale(image, detections);

	return detections;
}

// 内部的に使う変数
transient private Point pointTL = null; // 矩形左上の位置
transient private Point pointBR = null; // 矩形右下の位置
transient private Scalar colorScalar = null; // 色を示す値

/**
 * 検出結果の画像を取得する。
 * 
 * @param image 元の画像(Mat)
 * @param detections 検出結果(MatOfRect)
 * @param color 矩形描画色
 * @return 画像(Mat)
 */
public Mat getMatOfDetectingResult(Mat image, MatOfRect detections, Color color) {

	loadLibrary(); // 必ず最初に呼び出す

	if (image==null || detections==null) return null;

	Mat result = image.clone(); // 画像を複製

	if (pointTL==null) pointTL = new Point(0,0);
	if (pointBR==null) pointBR = new Point(0,0);
	if (colorScalar==null) colorScalar = new Scalar(0,0,0);

	// 色の変換
	colorScalar.val[2] = color.getRed();
	colorScalar.val[1] = color.getGreen();
	colorScalar.val[0] = color.getBlue();

	Rect[] rectArray =detections.toArray();
	for (int i=0; i<rectArray.length; i++) { // すべての検出結果に関して
		Rect rect = rectArray[i];
		pointTL.x = rect.x;
		pointTL.y = rect.y;
		pointBR.x = rect.x+rect.width;
		pointBR.y = rect.y+rect.height;
		// 画像に矩形描画
		Core.rectangle(result, pointTL, pointBR, colorScalar);
	}

	return result;
}

以上のようなメソッドを実装したコンポーネントを用いて、 MZ側で処理を記述した例を下図に示します。 ここでは、あらかじめ分類器をファイルからロードしておき、 USBカメラから取得した画像に対して物体検出を実施して、 検出結果の画像をイメージビューアに表示しています。

下図が実際に検出した結果の例です。 OpenCVに付属のlbpcascade_frontalface.xmlをロードして顔検出をしています。 (顔そのものは画像を修正してモザイク処理を施しています)

作成日 2013-11-25

最終更新日 2013-12-03