アンドロイドでOpenCVを使ってみる。
まずは、お約束の「顔検出」です。
目次
EclipseにOpenCV 2.3.1をインポート
サンプルを使って顔検出をやってみる
顔検出のサンプルコードにデータ保存用コードを追加
顔検出のマーカーを変更してみる
PC版はこちら
Android 2.3.1 for Windows
他のOpenCV関連ページNew
TOP
EclipseにOpenCVをインポート
OpenCV 2.3.1をライブラリとして使います。
環境は、Eclipse(INDIGO) + Android SDK + ADT
因みにWisteriaHillの環境はこんな感じ。
ターゲットはAndroid 2.3.3(API10)
1:OpenCV 2.3.1をダウンロードして解凍
Sourceforge(tarball)
OpenCV-2.3.1-android-bin.zip
ダウンロードしたものを解凍して、適当なドライブに移動。
2:Eclipseを起動して、インポート
[Existing Projects into Workspace]を選ぶ。
OpenCV 2.3.1のフォルダーを選んでOK。
OpenCV 2.3.1を右クリックして、Propertiesを設定。
Androidを選んで、Targetを2.3.3としてLibraryに登録。
以上です。
TOP
サンプルを使って顔検出をやってみる
解凍したOpenCV 2.3.1にsamplesフォルダーがあります。
再度、File->importでface_detectionフォルダーを選んでインポートします。
エラーが出る場合、OpenCV 2.3.1がライブラリとして認識されていない場合があります。
右クリックしてPropertiesを設定。
Android 2.3.3をターゲットにして、OpenCV 2.3.1をライブラリとして選んで、OK。
Sample-face-detectionがプロジェクト名です、実機にインストールして動作確認。
お顔が、緑の矩形で検出されます。
メニュには、Face size が20,30,40,50%の4種類あります。
多分画面に対するお顔のサイズの比率...かな。20%ならこのサイズも検出されます。
実行ファイルはここ(Sample - face-detection.apk)
6 11.7MBくらいの、結構デカいファイルですが、これはOpenCV 2.3.1がARM用の2種類のコードを持っているためです。
実機にデプロイして使う分には、armeabi-v7a(OpenCVのlibsフォルダー)だけでいいです。これだけなら、8.7MBくらいのサイズになります。
次は、この結果を使った、「お顔の認識」です。サーバーサイドと連携しましょう。
ところで、この「サーバーサイド」という言い方は良くないのかなあ、一般的じゃないのかも。
「クラウド」と、今後、言い換えましょうかね(実質、同じなんだけど....)
。
Under construction
以上。
TOP
顔検出のサンプルコードにデータ保存用コードを追加
2012/05/09 訂正
画面をタップして、
画像やデータの取得に、画面をタップするやり方を変更しました。
メニュに入れました、こんな感じ。
カメラ画像と、検出した際に表示される緑色の矩形の座標値をローカル(SDカード)に保存します。
ややこしくならないように、お顔の検出数は1つに制限しています。
//addや//modifyとついているところが追加部分。
新規プロジェクトから作成する場合
新規パッケージに内の以下のファイルを作成
Public Class -> FdView.java
Default Class -> FpsMeter.java
Public Abstract Class -> SampleCvViewBase.java
メインアクティビティのクラスにFdActivity.javaのコードを移植
AndroidManifest.xmlにカメラと外部ストレージを使えるようにパーミッション追加
res/rawフォルダーにカスケードファイルを置いておくこと
以下、ファイルのパスは適当に読み替えてください。
ニコちゃんマークを出すようになってますが、緑の矩形に戻す場合は、Core.rectangleを戻して、
int mwidthからCore.lineまでをコメントアウトしてください。
-----------------------------------------------------
【FdView.java】
package org.opencv.samples.fd;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
import org.opencv.android.Utils;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.highgui.Highgui;
import org.opencv.highgui.VideoCapture;
import org.opencv.objdetect.CascadeClassifier;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.view.SurfaceHolder;
import android.util.Log;
//add
import android.view.MotionEvent;
import android.os.Environment;
import android.graphics.Bitmap.CompressFormat;
import org.opencv.core.Point;
public class FdView extends SampleCvViewBase {
private static final String TAG = "Sample::FdView";
private Mat mRgba;
private Mat mGray;
private CascadeClassifier mCascade;
//add
private String RectCoordinate = "";
//add
private int tl_x = 0;
private int tl_y = 0;
private int br_x = 0;
private int br_y = 0;
private int prev_tl_x = 0;
private int prev_tl_y = 0;
private int prev_br_x = 0;
private int prev_br_y = 0;
public FdView(Context context) {
super(context);
try {
InputStream is = context.getResources().openRawResource(R.raw.lbpcascade_frontalface);
File cascadeDir = context.getDir("cascade", Context.MODE_PRIVATE);
File cascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");
FileOutputStream os = new FileOutputStream(cascadeFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
is.close();
os.close();
mCascade = new CascadeClassifier(cascadeFile.getAbsolutePath());
if (mCascade.empty()) {
Log.e(TAG, "Failed to load cascade classifier");
mCascade = null;
} else
Log.i(TAG, "Loaded cascade classifier from " + cascadeFile.getAbsolutePath());
cascadeFile.delete();
cascadeDir.delete();
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "Failed to load cascade. Exception thrown: " + e);
}
}
@Override
public void surfaceChanged(SurfaceHolder _holder, int format, int width, int height) {
super.surfaceChanged(_holder, format, width, height);
synchronized (this) {
// initialize Mats before usage
mGray = new Mat();
mRgba = new Mat();
}
}
@Override
protected Bitmap processFrame(VideoCapture capture) {
capture.retrieve(mRgba, Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGBA);
capture.retrieve(mGray, Highgui.CV_CAP_ANDROID_GREY_FRAME);
if (mCascade != null) {
int height = mGray.rows();
int faceSize = Math.round(height * FdActivity.minFaceSize);
List<Rect> faces = new LinkedList<Rect>();
mCascade.detectMultiScale(mGray, faces, 1.1, 2, 2 // TODO: objdetect.CV_HAAR_SCALE_IMAGE
, new Size(faceSize, faceSize));
int index = 0;//add
for (Rect r : faces){
//add
if (index == 0) {
/*
Core.rectangle(mRgba, r.tl(), r.br(), new Scalar(0, 255, 0, 255), 3);
*/
//add
int mwidth = (int)(r.br().x - r.tl().x);
int mheight = (int)(r.br().y - r.tl().y);
int c_x = (int)(r.tl().x + r.br().x)/2;
int c_y = (int)(r.tl().y + r.br().y)/2;
int radius = (int)(r.br().x - r.tl().x)/2;
Core.circle(mRgba, new Point(c_x,c_y), radius, new Scalar(200, 200, 200, 100), -1);
int ls_radius = (int)(mwidth * 0.3);
Core.ellipse(mRgba, new Point(c_x,c_y), new Size(ls_radius,ls_radius), 0, 0, 180, new Scalar(0, 0, 0, 255), 3);
int pt1_x = c_x - radius + 2;
int pt1_y = c_y + 5;
int pt2_x = c_x + radius - 2;
int pt2_y = c_y + 5;
Core.line(mRgba, new Point(pt1_x,pt1_y), new Point(pt2_x,pt2_y), new Scalar(255, 255, 255, 255),2);
tl_x = (int)r.tl().x;
tl_y = (int)r.tl().y;
br_x = (int)r.br().x;
br_y = (int)r.br().y;
if (prev_tl_x != 0) {
tl_x = (int)(tl_x * 0.1 + prev_tl_x * 0.9);
tl_y = (int)(tl_y * 0.1 + prev_tl_y * 0.9);
br_x = (int)(br_x * 0.1 + prev_br_x * 0.9);
br_y = (int)(br_y * 0.1 + prev_br_y * 0.9);
}
RectCoordinate = tl_x + "," + tl_y + "/" + br_x + "," + br_y;
prev_tl_x = tl_x;
prev_tl_y = tl_y;
prev_br_x = br_x;
prev_br_y = br_y;
}
}
}
Bitmap bmp = Bitmap.createBitmap(mRgba.cols(), mRgba.rows(), Bitmap.Config.ARGB_8888);
if (Utils.matToBitmap(mRgba, bmp))
return bmp;
bmp.recycle();
return null;
}
//add
//---------------------------------------
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
//get_cvPicture();
}
return true;
}
public void get_cvPicture() {
Bitmap bmp = null;
synchronized (this) {
if (mCamera.grab()) {
bmp = processFrame(mCamera);
}
}
if (bmp == null) {
return;
} else {
//String filePath = Environment.getExternalStorageDirectory() + "/DCIM/Camera/" + System.currentTimeMillis() + ".jpg";
String filePath4Picture = Environment.getExternalStorageDirectory() + "/DCIM/Camera/example.jpg";
String filePath4Data = Environment.getExternalStorageDirectory() + "/DCIM/Camera/example.txt";
try {
write_cvPicture(filePath4Picture, bmp);
write_cvData(filePath4Data, RectCoordinate);
//webView.loadUrl("javascript:show_alert('" + RectCoordinate + "')");
} catch (IOException e) {
//e.printStackTrace();
Log.e("err","Error:" + e.toString());
}
bmp.recycle();
}
}
private void write_cvPicture(String filePath, Bitmap bmp) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filePath);
bmp.compress(CompressFormat.JPEG, 100, out);
//
} finally {
if (out != null) {
out.close();
}
}
}
private void write_cvData(String filePath, String data) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filePath);
out.write(data.getBytes());
//
//
} finally {
if (out != null) {
out.close();
}
}
}
//---------------------------------------
@Override
public void run() {
super.run();
synchronized (this) {
// Explicitly deallocate Mats
if (mRgba != null)
mRgba.release();
if (mGray != null)
mGray.release();
mRgba = null;
mGray = null;
}
}
}
-----------------------------------------------------
【SampleCvViewBase.java】
package org.opencv.samples.fd;
import java.util.List;
import org.opencv.core.Size;
import org.opencv.highgui.VideoCapture;
import org.opencv.highgui.Highgui;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public abstract class SampleCvViewBase extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private static final String TAG = "Sample::SurfaceView";
private SurfaceHolder mHolder;
public VideoCapture mCamera;//modify
private FpsMeter mFps;
public SampleCvViewBase(Context context) {
super(context);
mHolder = getHolder();
mHolder.addCallback(this);
mFps = new FpsMeter();
Log.i(TAG, "Instantiated new " + this.getClass());
}
public void surfaceChanged(SurfaceHolder _holder, int format, int width, int height) {
Log.i(TAG, "surfaceCreated");
synchronized (this) {
if (mCamera != null && mCamera.isOpened()) {
Log.i(TAG, "before mCamera.getSupportedPreviewSizes()");
List<Size> sizes = mCamera.getSupportedPreviewSizes();
Log.i(TAG, "after mCamera.getSupportedPreviewSizes()");
int mFrameWidth = width;
int mFrameHeight = height;
// selecting optimal camera preview size
{
double minDiff = Double.MAX_VALUE;
for (Size size : sizes) {
if (Math.abs(size.height - height) < minDiff) {
mFrameWidth = (int) size.width;
mFrameHeight = (int) size.height;
minDiff = Math.abs(size.height - height);
}
}
}
mCamera.set(Highgui.CV_CAP_PROP_FRAME_WIDTH, mFrameWidth);
mCamera.set(Highgui.CV_CAP_PROP_FRAME_HEIGHT, mFrameHeight);
}
}
}
public void surfaceCreated(SurfaceHolder holder) {
Log.i(TAG, "surfaceCreated");
mCamera = new VideoCapture(Highgui.CV_CAP_ANDROID);
if (mCamera.isOpened()) {
(new Thread(this)).start();
} else {
mCamera.release();
mCamera = null;
Log.e(TAG, "Failed to open native camera");
}
}
public void surfaceDestroyed(SurfaceHolder holder) {
Log.i(TAG, "surfaceDestroyed");
if (mCamera != null) {
synchronized (this) {
mCamera.release();
mCamera = null;
}
}
}
protected abstract Bitmap processFrame(VideoCapture capture);
public void run() {
Log.i(TAG, "Starting processing thread");
mFps.init();
while (true) {
Bitmap bmp = null;
synchronized (this) {
if (mCamera == null)
break;
if (!mCamera.grab()) {
Log.e(TAG, "mCamera.grab() failed");
break;
}
bmp = processFrame(mCamera);
mFps.measure();
}
if (bmp != null) {
Canvas canvas = mHolder.lockCanvas();
if (canvas != null) {
canvas.drawBitmap(bmp, (canvas.getWidth() - bmp.getWidth()) / 2, (canvas.getHeight() - bmp.getHeight()) / 2, null);
mFps.draw(canvas, (canvas.getWidth() - bmp.getWidth()) / 2, 0);
mHolder.unlockCanvasAndPost(canvas);
}
bmp.recycle();
}
}
Log.i(TAG, "Finishing processing thread");
}
}
-----------------------------------------------------
【FdActivity.java】
package org.opencv.samples.fd;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.Window;
public class FdActivity extends Activity {
private static final String TAG = "Sample::Activity";
private MenuItem mItemFace50;
private MenuItem mItemFace40;
private MenuItem mItemFace30;
private MenuItem mItemFace20;
//add
private MenuItem mItemPicture;
public static float minFaceSize = 0.5f;
//add
private FdView fdv;
public FdActivity() {
Log.i(TAG, "Instantiated new " + this.getClass());
}
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate");
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
//modify
fdv = new FdView(this);
setContentView(fdv);
//setContentView(new FdView(this));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
Log.i(TAG, "onCreateOptionsMenu");
mItemFace50 = menu.add("Face size 50%");
mItemFace40 = menu.add("Face size 40%");
mItemFace30 = menu.add("Face size 30%");
mItemFace20 = menu.add("Face size 20%");
//add
mItemPicture = menu.add("Picture");
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.i(TAG, "Menu Item selected " + item);
if (item == mItemFace50)
minFaceSize = 0.5f;
else if (item == mItemFace40)
minFaceSize = 0.4f;
else if (item == mItemFace30)
minFaceSize = 0.3f;
else if (item == mItemFace20)
minFaceSize = 0.2f;
else if (item == mItemPicture)//add
take_photo();
return true;
}
//add
private void take_photo(){
fdv.get_cvPicture();
}
}
-----------------------------------------------------
【AndroidManifest.xml】
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.opencv.samples.fd"
android:versionCode="1"
android:versionName="1.0">
<supports-screens android:resizeable="true"
android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:anyDensity="true" />
<application android:label="@string/app_name" android:icon="@drawable/icon">
<activity android:name="FdActivity"
android:label="@string/app_name"
android:screenOrientation="landscape"
android:configChanges="keyboardHidden|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-sdk android:minSdkVersion="8" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
//add
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
</manifest>
サンプル
画像サンプル
データサンプル
サンプル内で、緑の矩形とデータが若干違うのは、データはフィルターをかけたものだからです。
画像がぼやけているのは、画面タッチが強すぎて、手ぶれしているからです。
手ぶれ補正などという高級なものは実装していません、ソフトにタッチすればもう少し鮮明に撮れます。
TOP
顔検出のマーカーを変更してみる
ニコニコマーク風にしてみます。
こんな感じ。
FdView.javaの
Core.rectangle(mRgba, r.tl(), r.br(), new Scalar(0, 255, 0, 255), 3);を以下のように変更。
int mwidth = (int)(r.br().x - r.tl().x);
int mheight = (int)(r.br().y - r.tl().y);
int c_x = (int)(r.tl().x + r.br().x)/2;
int c_y = (int)(r.tl().y + r.br().y)/2;
int radius = (int)(r.br().x - r.tl().x)/2;
Core.circle(mRgba, new Point(c_x,c_y), radius, new Scalar(200, 200, 200, 100), -1);
int ls_radius = (int)(mwidth * 0.3);
Core.ellipse(mRgba, new Point(c_x,c_y), new Size(ls_radius,ls_radius), 0, 0, 180, new Scalar(0, 0, 0, 255), 3);
int pt1_x = c_x - radius + 2;
int pt1_y = c_y + 5;
int pt2_x = c_x + radius - 2;
int pt2_y = c_y + 5;
Core.line(mRgba, new Point(pt1_x,pt1_y), new Point(pt2_x,pt2_y), new Scalar(255, 255, 255, 255),2);
ご一同さんおそろいで、ニコちゃんマークの図(ちょっとシュール)。
TOP
image_manipulationをやってみる
メニュは、こんな感じ
Canny(cannyを使ったエッジ検出)
Sepia
あまり効果が無かったので割愛
Sobel(sobelを使ったエッジ検出)
Blur(ぼやけ)
部分的にぼやかしてます。
Zoom(拡大)
赤枠の中を拡大
ちなみに、以下はTutorialにある、Cannyを使ったエッジ検出の例。
TOP
他のOpenCV関連ページ
OpenCV覚書
矩形領域の座標を取得するページ
OpenCV 2.3.1でカスケードを作って、Androidで使ってみる
アンドロイドでOpenCV(色検出)
Android OpenCV 2.3.1で画像認識
アンドロイドでOpenCV(特徴点検出)
AndroidでOpenCV 2.4.6を使ってみる
OpenCV + NyMMDで初音ミクさんにご挨拶してもらいます
Android OpenCV 2.4.6 の顔検出アプリを一から作ってみる