基于Java的Android OpenCV安装配置及人脸识别示例

1. OpenCV安装配置

1.1 开发环境

  • Android Studio 3.5.2
  • Opencv-4.3.0-android-sdk

1.2 安装OpenCV SDK

opencv的官网中下载android sdk,我这边下载的opencv-4.3.0
基于Java的Android OpenCV安装配置及人脸识别示例_第1张图片
将opencv-4.3.0-sdk解压到本地,解压后文件主要包括:
基于Java的Android OpenCV安装配置及人脸识别示例_第2张图片

1.3 创建android项目

选择带有c++开发环境的Native C++模板,创建项目
基于Java的Android OpenCV安装配置及人脸识别示例_第3张图片
C++Standard选择 c++14
创建好项目如下:
基于Java的Android OpenCV安装配置及人脸识别示例_第4张图片
其中cpp文件夹下包括: include,CMakeLists.txt,native-lib文件
点击File -> Setting -> Android SDK->SDK Tools,勾选好NDK,CMAKE
基于Java的Android OpenCV安装配置及人脸识别示例_第5张图片
点击Show Package Details可选择详细的版本进行安装
基于Java的Android OpenCV安装配置及人脸识别示例_第6张图片
如果NDK报错,可能是版本太高了。选择File -> Project Structure,配置低版本的NDK的路径,这里选择ndk-r16b
基于Java的Android OpenCV安装配置及人脸识别示例_第7张图片

1.4 添加OpenCV开发包支持

通过选择File -> New -> import Module
基于Java的Android OpenCV安装配置及人脸识别示例_第8张图片

选择opencv-android-sdk/sdk/java,选择sdk下面的java目录。Module name命名为OpenCVLibrary430,选择Next默认,Finsh.
基于Java的Android OpenCV安装配置及人脸识别示例_第9张图片
基于Java的Android OpenCV安装配置及人脸识别示例_第10张图片
创建好后,成功导入了OpenCVLibrary430库。
将项目切换为Project模式,选择OpenCV Library430 ->build gradle将 build gradle第一行的

apply plugin: 'com.android.application'

改为:

apply plugin: 'com.android.library'

删除defaultConfig 中的applicationId
选择File -> Project Structure ->Dependencies可以看到OpenCVLibrary430前面变成了库文件的图标。
基于Java的Android OpenCV安装配置及人脸识别示例_第11张图片

1.5 创建JniLibs

选择app -> src ->main 在main文件中右键选择New -> Folder ->JNI Folder,创建Jni文件
基于Java的Android OpenCV安装配置及人脸识别示例_第12张图片
基于Java的Android OpenCV安装配置及人脸识别示例_第13张图片
将opencv-android-sdk下面的sdk ->native ->libs中的文件拷贝到JNI文件中。

然后选中File -> Project Structure ->Dependencies,点击app选择+号,添加Module Dependency,将OpenCVLibrary430库添加到app中

基于Java的Android OpenCV安装配置及人脸识别示例_第14张图片将OpenCVLibrary430中的compileSdkVersionbuildToolVersion的版本设置与app中build.gradle的compileSdkVersionbuildToolVersion的版本一致。可以在Project Structure中的Modules中进行设置。
基于Java的Android OpenCV安装配置及人脸识别示例_第15张图片
基于Java的Android OpenCV安装配置及人脸识别示例_第16张图片
查看app所属的build.gradle,将

   sourceSets { main { jni.srcDirs = ['src/main/jni', 'src/main/jniLibs'] } }

改为

   sourceSets { main { jniLibs.srcDirs = [ 'src/main/jniLibs'] } }

注: 如果提示OpenCV error: Cannot load info library for OpenCV,说明系统找不到OpenCV的so文件。确认是否把jniLibs.srcDirs错误写成了jni.srcDirs。因为我们配置的是jniLibs,所以需要写成jniLibs.srcDirs

查看dependencies中是否进入了OpenCVLibrary430基于Java的Android OpenCV安装配置及人脸识别示例_第17张图片
并且,在defaultCofig中的externalNativeBuild配置cmake参数

  externalNativeBuild {
            cmake {
                cppFlags "-std=c++14 -frtti  -fexceptions"
                abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
                arguments 'DANDROID_STL=c++_shared'
            }
        }

环境配置好后,运行程序,如果程序运行没出错,说明已经正确添加好程序。

注:如果提示ERROR: Cause: exception while building Json D:\works\Android_cv\project\OpenCVCplus\app\.cxx\cmake\debug\x86\CMakeFiles\CMakeTmp\.ninja_deps: 另一个程序正在使用此文件,进程无法访问。通过删除app文件下的 .cxxbuild文件重新运行即可。
基于Java的Android OpenCV安装配置及人脸识别示例_第18张图片

2. 人脸识别项目

2.1 页面布局

activity_main.xml


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:id="@+id/loadButton"
        android:text="load"/>
    <Button
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:layout_below="@id/loadButton"
        android:id="@+id/processButton"
        android:text="Detect Face"/>
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:id="@+id/imageView1"
        android:layout_below="@+id/processButton"/>
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:id="@+id/imageView2"
        android:layout_below="@+id/imageView1"/>
RelativeLayout>

界面效果:
基于Java的Android OpenCV安装配置及人脸识别示例_第19张图片

2.2 存放模型资源文件

创建res资源文件下创建raw文件用来存放人脸识别的lbpcascade_frontalface.xml模型文件。
方法:res ->New ->Android Resource Directory
基于Java的Android OpenCV安装配置及人脸识别示例_第20张图片

设置:Directory name为raw, Resource type 下拉选择raw。然后将模型资源文件lbpcascade_frontalface.xml粘贴到该文件下。
基于Java的Android OpenCV安装配置及人脸识别示例_第21张图片
基于Java的Android OpenCV安装配置及人脸识别示例_第22张图片

2.3 MainActivity.java

代码

package com.example.myopencvdemo;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfRect;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.CascadeClassifier;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;

public class MainActivity extends AppCompatActivity {

    Button loadButton;
    Button detectFaceButton;

    ImageView imageView1;
    ImageView imageView2;
    String imagePathLoaded;
    Mat matrix;
    Bitmap bitmap;


    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
//        OpenCVLoader.initDebug();
        ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},200);
       loadButton=findViewById(R.id.loadButton);
       detectFaceButton=findViewById(R.id.processButton);
       imageView1=findViewById(R.id.imageView1);
       imageView2=findViewById(R.id.imageView2);

       loadButton.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View view) {
               Intent intent=new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI);
               //打开一个带有返回值的交互界面
               startActivityForResult(intent,300); //300 requestCode设置为300
           }
       });

       detectFaceButton.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View view) {
              detectFace();
           }
       });
    }


    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data){
        super.onActivityResult(requestCode,resultCode,data);
        if(requestCode == 300){
            if(resultCode == RESULT_OK){
                Uri uri=data.getData();
                imageView1.setImageURI(uri);
                imagePathLoaded=getPathFromURI(MainActivity.this,uri);

                try {
                    FileInputStream fis = new FileInputStream(imagePathLoaded);
                    bitmap=BitmapFactory.decodeStream(fis);
                    //imageView1.setImageBitmap(bitmap);

                } catch (Exception e) {
                    e.printStackTrace();
                }
//                matrix= Imgcodecs.imread(imagePathLoaded);
            }
        }
    }
    // 根据相册的Uri获取图片的路径
    public static String getPathFromURI(Context context, Uri uri) {
        String result;
        Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
        if (cursor == null) {
            result = uri.getPath();
        } else {
            cursor.moveToFirst();
            int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
            result = cursor.getString(idx);
            cursor.close();
        }
        return result;
    }

    public void detectFace(){
        if(bitmap == null)
        {
            return ;
        }
        Bitmap bit=bitmap.copy(Bitmap.Config.ARGB_8888,false);
        Mat src=new Mat(bit.getHeight(),bit.getWidth(), CvType.CV_8UC(3));

        Utils.bitmapToMat(bit,src);
        Mat dst=src.clone();
        Imgproc.cvtColor(src,dst, Imgproc.COLOR_BGR2GRAY);
        Mat matrix =dst.clone();

        CascadeClassifier cascadeClassifier=new CascadeClassifier();

        try {
            //Copy the resource into a temp file so OpenCV can load it
            InputStream is=this.getResources().openRawResource(R.raw.lbpcascade_frontalface);
            //在内存中创建名为cascade的目录  Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问
            File cascadeDir=getDir("cascade", Context.MODE_PRIVATE);
            //android 中raw文件下面的模型文件不支持直接读取,我们需要把模型保存到一个缓存目录,然后再从缓存目录加载模。
            File mCascadeFile = new File(cascadeDir,"lbpcascade_frontalface.xml");
           // 创建一个向指定 File 对象表示的文件中写入数据的文件输出流
            FileOutputStream os=new FileOutputStream((mCascadeFile));
            byte[] buffer=new byte[4096];
            int bytesRead;
            while((bytesRead = is.read(buffer))!=-1){
                os.write(buffer,0,bytesRead);
            }
            is.close();
            os.close();

            //Load the cascade classifer
            cascadeClassifier=new CascadeClassifier(mCascadeFile.getAbsolutePath());

        }catch (Exception e){
            Log.e("OpenCVActivity","Error loading casce",e);
        }

        MatOfRect faceArray = new MatOfRect();
        cascadeClassifier.detectMultiScale(matrix,faceArray);

        int numFaces=faceArray.toArray().length;
        for(Rect face:faceArray.toArray()){
            Imgproc.rectangle(src,new Point(face.x,face.y),new Point(face.x+face.width,face.y+face.height),new Scalar(0,0,255),3);
        }

        Mat finalMatrix = src.clone();
        Bitmap bitmap =Bitmap.createBitmap(finalMatrix.cols(),finalMatrix.rows(),Bitmap.Config.ARGB_8888);
        Utils.matToBitmap(finalMatrix,bitmap);
        imageView2.setImageBitmap(bitmap);

        Toast.makeText(getApplicationContext(),numFaces+ " faces found!",Toast.LENGTH_SHORT).show();
    }


	//需要在onResume方法中验证是否可以使用内置的OpenCV库
    @Override
    protected void onResume() {
        super.onResume();
        if(!OpenCVLoader.initDebug()){
            Log.i("cv","未收到内置OpenCV库,使用OpenCV Manager进行初始化。");

//            OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_4_0, this, mLoaderCallback);
        }else
        {
            Log.i("cv","发现了内置OpenCV库,使用OpenCV 库进行初始化。");
//            mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
        }
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}

  • android 中raw文件下面的模型文件不支持直接读取,我们需要把模型保存到一个缓存目录,然后再从缓存目录加载模。
  • 片的地址就包括 MediaStore.Images.Media.INTERNAL_CONTENT_URIMediaStore.Images.Media.EXTERNAL_CONTENT_URI 两个基础地址。其值分别是 content://media/internal/images/media 和 content://media/external/images/media ,对应内部库和外部库地址。每一张图片的地址基本上是上面的基础URL地址下加上图片的内部ID。打个比方一张存储卡上的图片ID为2,其对应的Uri地址就是 content://media/external/images/media/2. 知道了这个地址,基本上就可以操作这张图片的所有信息了。参考
  • startActivityForResult主要用来从FirstActivity跳转到SecondActivity然后返回FirstActivity并且获取从SecondActivity传回来的参数。参考

2.4 人脸识别效果

点击LOAD按钮导入本地一张带有人脸的图片,点击DETECT按钮就可以识别出人脸,效果如下:
基于Java的Android OpenCV安装配置及人脸识别示例_第23张图片
该项目github中的源码地址为: https://github.com/yuanxinshui/TFLiteClassificationPic/tree/main/OpenCVFaceDetect

你可能感兴趣的