2017-02-10  Android Java プログラミング

AndroidでOpenCV 3.2を使って顔検出をする

OpenCV Intel が公開しているオープンソースの画像解析ライブラリです。今回は Android から OpenCV を使ってカメラ映像から人の顔を検出するプログラムを作ってみます。使用する OpenCV のバージョンは昨年末にリリースされたばかりの 3.2 を使用します。

OpenCV for Androidをダウンロードする

OpenCV のサイトでは Windows Linux Mac Android iOS と様々な OS 用のライブラリがダウンロードできるようになっています。バイナリーが提供されているのでソースコードからコンパイルするといった手間もなく簡単に使い始めることができます。

下記リンクから OpenCV のサイトを開きます。

OpenCV for Android というリンクをクリックするとダウンロードが始まります。opencv-3.2.0-android-sdk.zip のダウンロードが完了したら適当な場所に展開します。

以下 E:\OpenCV-android-sdk に展開した前提で説明をしていきます。

Android Studioで新規プロジェクトを作成する

Android Studio で新規プロジェクトを作成していきます。

アプリケーションの名前やパッケージ名は適当でかまいません。

ターゲットプラットフォームもお好みで。私は自分の持っているスマートフォンに合わせて API 22: Android 5.1 (Lolipop) を選択しました。

アクティビティはシンプルな Empty Activity を選択しました。

Backwards Compatibility (AppCompat) のチェックを外すと プロジェクト構成がよりシンプルになります。

OpenCVをモジュールとして追加する

Android Studio プロジェクトのひな型ができたら OpenCV をモジュールとして追加します。

FileNewImport Module... を選択して New Module ウィザードを起動します。Source directory: の右側にあるボタンを押して OpenCV ライブラリを展開したフォルダー下位にある sdk\java を指定します。Module name: には自動的に openCVLibrary320 と表示されるので このまま Next を押します。

次の画面では 少し長い英文が表示されてチェックボックスが 3 つ並んでいます。チェックを入れたままにしておけば build.gradle を自動的に書き換えてライブラリの依存関係を追加してくれます。そのまま Finish を押しましょう。

これで プロジェクトに OpenCV モジュールが追加れました。

libopencv_java3.soをコピーする

ZIP ファイルを展開した E:\OpenCV-android-sdk\sdk\native\libs 以下に armeabi-v7a などの CPU アーキテクチャ名の付いたフォルダーがあり その中に libopencv_*.a という拡張子 .a の付いた複数のファイルと libopencv_java3.so というファイルがあります。必要になるのは libopencv_java3.so というファイルです。

これを CPU アーキテクチャ名の付いたフォルダーごとプロジェクトの app/src/main/jniLibs にコピーします。jniLibs というフォルダーは存在していないので作成してください

自分が必要としている CPU アーキテクチャの分だけコピーしてください。私は armeabi-v7a だけをコピーしました。

haarcascade_frontalface_alt.xmlをコピーする

ZIP ファイルを展開した E:\OpenCV-android-sdk\sdk\etc\haarcascades 以下に複数の XML があります。今回は顔検出をするために haarcascade_frontalface_alt.xml を使用します。このファイルを haarcascades フォルダーと一緒にプロジェクトの assets にコピーします。assets main を右クリックして NewFolderAssets Folder を選ぶことで作成できます。

appにモジュール依存関係を設定する

FileProject Structure のサイトを開きます。左側の app を選択してから右側の Dependencies タブを開きます。右端の + ボタンを押して Module dependency を選択するとモジュール選択の一覧が表示されます。openCVLibrary320 を選択状態にして OK を押します。

もう一度 OK を押して Project Structure を閉じます。

compileSdkVersionを修正する

まだプログラムコードはまったく書いていませんが ここで一度ビルドをしてみます。BuildMake Project を選択します。

するとエラーがずらっと表示されました。

エラー: シンボルを見つけられません
シンボル: 変数 GL_TEXTURE_EXTERNAL_OES
場所: クラス GLES11Ext

このようなエラーが表示された場合は build.gradle (Module: openCVLibrary320) を開いて compileSdkVersion の値を確認してみてください。バージョン指定が 21 未満の場合は 21 以上の値に修正してください。

もう一度 BuildMake Project を選択して エラーが解消していれば大丈夫です。

プログラムを作る

ここまでの手順で OpenCV を使う準備が整いました。ようやくプログラムを書き始めることができます。

このプログラムではカメラデバイスを使うので AndroidManifest.xml には CAMERA パーミッションを追加しておいてください。

AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA"/>

先に完成しているソースコードを載せます。

MainActivity.java
package com.example.opencv; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class MainActivity extends Activity { static { System.loadLibrary("opencv_java3"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); try { // assetsの内容を /data/data/*/files/ にコピーします。 copyAssets("haarcascades"); } catch (IOException e) { e.printStackTrace(); } CameraView cameraView = new CameraView(this, 90); ViewGroup activityMain = (ViewGroup)findViewById(R.id.activity_main); activityMain.addView(cameraView); } private void copyAssets(String dir) throws IOException { byte[] buf = new byte[8192]; int size; File dst = new File(getFilesDir(), dir); if(!dst.exists()) { dst.mkdirs(); dst.setReadable(true, false); dst.setWritable(true, false); dst.setExecutable(true, false); } for(String filename : getAssets().list(dir)) { File file = new File(dst, filename); OutputStream out = new FileOutputStream(file); InputStream in = getAssets().open(dir + "/" + filename); while((size = in.read(buf)) >= 0) { if(size > 0) { out.write(buf, 0, size); } } in.close(); out.close(); file.setReadable(true, false); file.setWritable(true, false); file.setExecutable(true, false); } } }

MainActivity では特に難しいことはしていません。

1. opencv_java3.soのロード

以下のコードで opencv\_java3.so をロードしています。

static {
    System.loadLibrary("opencv_java3");
}

2. assetsからfilesへのファイルコピー

copyAssets メソッドを作って assets から files にファイルをコピーしています。

3. CameraView の追加

アクティビティに CameraView (後述)を追加しています。CameraView の追加は Java コードでやっているので res/layout/activity_main.xml を編集する必要はありません。

CameraView.java
package com.example.opencv; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.hardware.Camera; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import org.opencv.android.Utils; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.MatOfRect; import org.opencv.core.Scalar; import org.opencv.objdetect.CascadeClassifier; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class CameraView extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback { private static final String TAG = "CameraView"; private int degrees; private Camera camera; private int[] rgb; private Bitmap bitmap; private Mat image; private CascadeClassifier detector; private MatOfRect objects; private List<RectF> faces = new ArrayList<RectF>(); public CameraView(Context context, int displayOrientationDegrees) { super(context); setWillNotDraw(false); getHolder().addCallback(this); String filename = context.getFilesDir().getAbsolutePath() + "/haarcascades/haarcascade_frontalface_alt.xml"; detector = new CascadeClassifier(filename); objects = new MatOfRect(); degrees = displayOrientationDegrees; } /* * SurfaceHolder.Callback */ @Override public void surfaceCreated(SurfaceHolder holder) { Log.d(TAG, "surfaceCreated: holder=" + holder); if(camera == null) { camera = Camera.open(0); } camera.setDisplayOrientation(degrees); camera.setPreviewCallback(this); try { camera.setPreviewDisplay(holder); } catch(IOException e) { e.printStackTrace(); } Camera.Parameters params = camera.getParameters(); for(Camera.Size size : params.getSupportedPreviewSizes()) { Log.i(TAG, "preview size: " + size.width + "x" + size.height); } for(Camera.Size size : params.getSupportedPictureSizes()) { Log.i(TAG, "picture size: " + size.width + "x" + size.height); } params.setPreviewSize(640, 480); camera.setParameters(params); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.d(TAG, "surfaceChanged: holder=" + holder + ", format=" + format + ", width=" + width + ", height=" + height); if(image != null) { image.release(); image = null; } if(bitmap != null) { if(!bitmap.isRecycled()) { bitmap.recycle(); } bitmap = null; } if(rgb != null) { rgb = null; } faces.clear(); camera.startPreview(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.d(TAG, "surfaceDestroyed: holder=" + holder); if(camera != null) { camera.stopPreview(); camera.release(); camera = null; } if(image != null) { image.release(); image = null; } if(bitmap != null) { if(!bitmap.isRecycled()) { bitmap.recycle(); } bitmap = null; } if(rgb != null) { rgb = null; } faces.clear(); } /* * SurfaceHolder.Callback */ @Override public void onPreviewFrame(byte[] data, Camera camera) { //Log.d(TAG, "onPreviewFrame: "); int width = camera.getParameters().getPreviewSize().width; int height = camera.getParameters().getPreviewSize().height; Log.d(TAG, "onPreviewFrame: width=" + width + ", height=" + height); Bitmap bitmap = decode(data, width, height, degrees); if(degrees == 90) { int tmp = width; width = height; height = tmp; } if(image == null) { image = new Mat(height, width, CvType.CV_8U, new Scalar(4)); } Utils.bitmapToMat(bitmap, image); detector.detectMultiScale(image, objects); faces.clear(); for(org.opencv.core.Rect rect : objects.toArray()) { float left = (float)(1.0 * rect.x / width); float top = (float)(1.0 * rect.y / height); float right = left + (float)(1.0 * rect.width / width); float bottom = top + (float)(1.0 * rect.height / height); faces.add(new RectF(left, top, right, bottom)); } invalidate(); } /* * View */ @Override protected void onDraw(Canvas canvas) { //もともとSurfaceView は setWillNotDraw(true) なので //super.onDraw(canvas) を呼ばなくてもよい。 //super.onDraw(canvas); Paint paint = new Paint(); paint.setColor(Color.GREEN); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(4); int width = getWidth(); int height = getHeight(); for(RectF face : faces) { RectF r = new RectF(width * face.left, height * face.top, width * face.right, height * face.bottom); canvas.drawRect(r, paint); } } /** Camera.PreviewCallback.onPreviewFrame で渡されたデータを Bitmap に変換します。 * * @param data * @param width * @param height * @param degrees * @return */ private Bitmap decode(byte[] data, int width, int height, int degrees) { if (rgb == null) { rgb = new int[width * height]; } final int frameSize = width * height; for (int j = 0, yp = 0; j < height; j++) { int uvp = frameSize + (j >> 1) * width, u = 0, v = 0; for (int i = 0; i < width; i++, yp++) { int y = (0xff & ((int) data[yp])) - 16; if (y < 0) y = 0; if ((i & 1) == 0) { v = (0xff & data[uvp++]) - 128; u = (0xff & data[uvp++]) - 128; } int y1192 = 1192 * y; int r = (y1192 + 1634 * v); int g = (y1192 - 833 * v - 400 * u); int b = (y1192 + 2066 * u); if (r < 0) r = 0; else if (r > 262143) r = 262143; if (g < 0) g = 0; else if (g > 262143) g = 262143; if (b < 0) b = 0; else if (b > 262143) b = 262143; rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff); } } if(degrees == 90) { int[] rotatedData = new int[rgb.length]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { rotatedData[x * height + height - y - 1] = rgb[x + y * width]; } } int tmp = width; width = height; height = tmp; rgb = rotatedData; } if(bitmap == null) { bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); } bitmap.setPixels(rgb, 0, width, 0, 0, width, height); return bitmap; } }

android.hardware.Camera を使ってカメラのプレビュー画像を onPreviewFrame で受け取っています。受け取ったデータを Bitmap に変換して detector OpenCV CascadeClassifier に渡すことで顔認識ができます。

顔認識した領域 矩形 が取得できるので それを onDraw で描画しています。

haarcascades XMLとは?

今回は顔認識をするために haarcascade_frontalface_alt.xml を使用しましたが OpenCV には他にも様々な XML ファイルが用意されています。これらを使うことで いろいろな画像解析をおこなうことができるようです。

また 顔認識をおこなうための XML も複数用意されています。

  • haarcascade_frontalface_alt.xml
  • haarcascade_frontalface_alt_tree.xml
  • haarcascade_frontalface_alt2.xml
  • haarcascade_frontalface_default.xml

どれを使っても顔を検出することができますが それぞれ検出率や精度が異なるようです。仕組みの違いに関するドキュメントを見つけることはできませんでしたが 検出精度について Stack Overflow それぞれの XML で検出率を比較した投稿を見つけることができました。

これによると haarcascade_frontalface_default.xml は認識率は高いが誤認識もある つまりゆるいようです。haarcascade_frontalface_alt_tree.xml は認識率がだいぶ下がってしまう。

OpenCV 3.2 の導入とサンプルコードは以上です。

そのうち 顔認識した結果からその人物までの距離を推定するといったことにも挑戦してみようと思います。

最終更新日 2024-12-13
この記事を共有しませんか?
ブックマーク ポスト