深入OpenCV Android应用开发 中文版 – 第四章代码更新

本书中文版购书链接:京东  当当

英文原版购书链接:亚马逊《Mastering OpenCV Android Application Programming》


        原书所给代码以章为单位,针对的Android版本从API 19到API 21不等,同时使用的OpenCV库版本也有2.4.9和2.4.10两种。本文给出的代码是在原书代码的基础上,针对Android 7.0(API 24)与OpenCV 3.2进行了修改,适当地增加了一些预处理等操作,以使代码整体上更合理。

        原书的完整代码可以在这里获取:https://www.packtpub.com/lcode_download/22299

        更新后的代码托管在GitHub:https://github.com/johnhany/MOAAP/tree/master/MOAAP-Chp4-r3

        关于在Android Studio上配置OpenCV开发环境的方法,请参考《在Android Studio上进行OpenCV 3.1开发》

        本章介绍如何在Android Studio 2上利用OpenCV开发一个人脸检测应用,涉及的算法有Haar级联分类器和LBP级联分类器。关于以上各算法的原理可以参考《深入OpenCV Android应用开发》第四章。


开发环境

        Windows 10 x64

        Android Studio 2.3.3(Gradle 3.3,Android Plugin 2.3.3)

        Android 7.0(API 24)

        JDK 8u141

        OpenCV 3.2.0 Android SDK


代码及简略解释

        1. 创建Android Studio项目,包命名为net.johnhany.moaap_chp4。导入OpenCV-android-sdk\sdk\java到项目中,并为app模块加载模块依赖。

        2.app\src\main\java\net\johnhany\moaap_chp4\MainActivity.java文件修改为如下内容:

package net.johnhany.moaap_chp4;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.widget.Toast;

import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.core.MatOfRect;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.CascadeClassifier;

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

public class MainActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener2 {

    private static final String TAG = "OCVSample::Activity";
    private CameraBridgeViewBase mOpenCvCameraView;
    private boolean              mIsFrontCamera = false;
    private MenuItem             mItemSwitchCamera = null;
    private Mat mRgba;
    private CascadeClassifier haarCascade;
    static int REQUEST_CAMERA = 0;
    static boolean read_external_storage_granted = false;

    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                {
                    Log.i(TAG, "OpenCV成功加载");
                    try{
                        InputStream is = getResources().openRawResource(R.raw.haarcascade_frontalface_alt2);
                        File cascadeDir = getDir("cascade",
                                Context.MODE_PRIVATE);
                        File mCascadeFile = new File(cascadeDir,"cascade.xml");
                        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();

                        haarCascade = new CascadeClassifier(
                                mCascadeFile.getAbsolutePath());
                        if (haarCascade.empty())
                        {
                            Log.i("Cascade Error", "级联分类器加载失败");
                            haarCascade = null;
                        }
                    }
                    catch(Exception e)
                    {
                        Log.i("Cascade Error: ","找不到级联分类器文件");
                    }
                    mOpenCvCameraView.enableView();
                } break;
                default:
                {
                    super.onManagerConnected(status);
                } break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "called onCreate");
        super.onCreate(savedInstanceState);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        setContentView(R.layout.activity_main);

        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            Log.i("permission", "request READ_EXTERNAL_STORAGE");
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
        }else {
            Log.i("permission", "READ_EXTERNAL_STORAGE already granted");
            read_external_storage_granted = true;
        }

        mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.java_surface_view);
        mOpenCvCameraView.setCvCameraViewListener(this);

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        Log.i(TAG, "called onCreateOptionsMenu");
        mItemSwitchCamera = menu.add("切换前置/后置摄像头");
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        String toastMessage;

        if (item == mItemSwitchCamera) {
            mOpenCvCameraView.setVisibility(SurfaceView.GONE);
            mIsFrontCamera = !mIsFrontCamera;

            if (mIsFrontCamera) {
                mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.java_surface_view);
                mOpenCvCameraView.setCameraIndex(1);
                toastMessage = "Front Camera";
            } else {
                mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.java_surface_view);
                mOpenCvCameraView.setCameraIndex(-1);
                toastMessage = "Back Camera";
            }

            mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);
            mOpenCvCameraView.setCvCameraViewListener(this);
            mOpenCvCameraView.enableView();
            Toast toast = Toast.makeText(this, toastMessage, Toast.LENGTH_LONG);
            toast.show();
        }

        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
        if (requestCode == REQUEST_CAMERA) {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // permission was granted
                Log.i("permission", "CAMERA granted");
                read_external_storage_granted = true;
            } else {
                // permission denied
                Log.i("permission", "CAMERA denied");
            }
        }
    }

    @Override
    public void onCameraViewStarted(int width, int height) {
        mRgba = new Mat(height, width, CvType.CV_8UC4);
    }

    @Override
    public void onCameraViewStopped() {

    }

    @Override
    public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
        //Rotating the input frame
        Mat mGray = inputFrame.gray();
        mRgba = inputFrame.rgba();
        if (mIsFrontCamera)
        {
            Core.flip(mRgba, mRgba, 1);
            Core.flip(mGray, mGray, 1);
        }

        //Detecting face in the frame
        MatOfRect faces = new MatOfRect();
        if(haarCascade != null)
        {
            haarCascade.detectMultiScale(mGray, faces, 1.1, 2, 2, new Size(200,200), new Size());
        }

        Rect[] facesArray = faces.toArray();
        for (int i = 0; i < facesArray.length; i++)
            Imgproc.rectangle(mRgba, facesArray[i].tl(), facesArray[i].br(), new Scalar(100), 3);
        return mRgba;
    }

    @Override
    public void onPause()
    {
        super.onPause();
        if (mOpenCvCameraView != null)
            mOpenCvCameraView.disableView();
    }

    @Override
    public void onResume()
    {
        super.onResume();
        OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_2_0, this, mLoaderCallback);
    }

    public void onDestroy() {
        super.onDestroy();
        if (mOpenCvCameraView != null)
            mOpenCvCameraView.disableView();
    }
}

        第48行的R.raw.haarcascade_frontalface_alt2代表预训练的分类器文件。该文件从OpenCV-android-sdk\sdk\etc\haarcascades可以找到,然后复制到项目目录的app\src\main\res\raw文件夹中(需要自己建立一个名为raw的文件夹)。在OpenCV-android-sdk\sdk\etc目录中,有两个文件夹,一个是haarcascades,包含官方提供的已经训练好的各种Haar分类器(更准确地说,是分类器的参数),可以直接用CascadeClassifier()调用进行分类;另一个是lbpcascades,包含多种预训练LBP分类器。如果想把Haar换为LBP,那么把haarcascade_frontalface_alt2替换为LBP的文件名。两种分类器都能被CascadeClassifier()加载,只不过LBP的速度稍快于Haar,而且精度低于Haar。

        3. 修改app\src\main\res\layout\activity_main.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="net.johnhany.moaap_chp4.MainActivity">

    <org.opencv.android.JavaCameraView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:id="@+id/java_surface_view" />

</LinearLayout>

        4. 在app\src\main\res目录下创建一个名为menu的Andorid resource directory,再在res\menu中创建一个menu_main.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/action_settings" android:title="@string/action_settings"
        android:orderInCategory="100" app:showAsAction="never" />
</menu>

        其实这个action_settings按钮并没有实际用途,老版本的Android Studio会默认创建这一项,我放在这儿只是为了占位:)但menu根节点是不可或缺的,因为我们在Java代码中添加了一个切换前置和后置摄像头的按钮。

        5. 修改app\src\main\res\AndroidManifest.xml文件为如下内容:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.johnhany.moaap_chp4">

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-feature android:name="android.hardware.camera" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.front" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="landscape">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

        6. 修改app\src\main\res\values\strings.xml文件:

<resources>
    <string name="app_name">第四章 - 深入OpenCV Android应用开发</string>
    <string name="action_settings">Settings</string>
</resources>

        同样,第二项Settings其实是无用的。

        7. 修改app\src\main\res\values\styles.xml文件:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowActionBar">true</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

        这里保留ActionBar而没有完全全屏的原因是菜单按钮一般位于ActionBar,我们还需要通过菜单按钮来切换前/后置摄像头。

        8. app\build.gradle文件修改为:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "26.0.1"
    defaultConfig {
        applicationId "net.johnhany.moaap_chp4"
        minSdkVersion 16
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    compile project(':openCVLibrary320')
}

        9. openCVLibrary320\build.gradle文件修改为:

apply plugin: 'com.android.library'

android {
    compileSdkVersion 24
    buildToolsVersion "26.0.1"

    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 24
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }
}

        10. 检查一下项目根目录的build.gradle文件是否为如下内容:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

运行效果

Subscribe
订阅评论
guest
2 评论
最新
最旧 得票最多
Inline Feedbacks
View all comments
shadow_wxh
shadow_wxh
2 年 之前

第185行加一句

Core.flip(mGray, mGray, 1);

不然正面的标记会错位