英文原版购书链接:亚马逊《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 }
运行效果
第185行加一句
Core.flip(mGray, mGray, 1);
不然正面的标记会错位
多谢指正!问题已经修复