1 概述
完成 Android 相機預覽功能以后,在此基礎上我使用 dlib 與 opencv 庫做了一個關于人臉檢測的 demo。該 demo 在相機預覽過程中對人臉進行實時檢測,并將檢測到的人臉用矩形框描繪出來。具體實現原理如下:
采用雙層 View,底層的 TextureView 用于預覽,程序從 TextureView 中獲取預覽幀數據,然后調用 dlib 庫對幀數據進行處理,最后將檢測結果繪制在頂層的 SurfaceView 中。
2 項目配置
由于項目中用到了 dlib 與 opencv 庫,因此需要對其進行配置。主要涉及到以下幾個方面:
2.1 C++支持
在項目創建過程中依次選擇 Include C++ Support、C++11、Exceptions Support ( -fexceptions )以及 Runtime Type Information Support ( -frtti ) 。最后生成的 build.gradle 文件如下:
defaultConfig?{ ?applicationId?"com.example.lightweh.facedetection" ?minSdkVersion?23 ?targetSdkVersion?28 ?versionCode?1 ?versionName?"1.0" ?testInstrumentationRunner?"android.support.test.runner.AndroidJUnitRunner" ?externalNativeBuild?{ ?cmake?{?arguments?"-DCMAKE_BUILD_TYPE=Release" ?cppFlags?"-std=c++11?-frtti?-fexceptions" ?} ?} }
其中,arguments 參數是后添加上去的,主要用于指定 CMake 的編譯模式為 Release,因為在 Debug 模式下 dlib 庫中相關算法的運行速度非常慢。前期如果需要調試 C++ 代碼,可先將 arguments 參數注釋。
2.2 dlib 與 opencv 下載
到dlib官網下載最新版本的源碼,解壓后將文件夾中的dlib目錄復制到 Android Studio 工程的 cpp 目錄下。
到 sourceforge 下載最新的 opencv-android 庫,解壓后將文件夾中的 native 目錄同樣復制到 Android Studio 工程的 cpp 目錄下,并改名為 opencv。
2.3 CMakeLists 配置
在 CMakeLists 文件中,我們首先包含 dlib 的 cmake 文件,接下來添加 opencv 的 include 文件夾并引入 opencv 的 so 庫,同時將 jni_common 目錄中的文件及人臉檢測相關文件添加至 native-lib 庫中,最后進行鏈接。
#?設置native目錄set(NATIVE_DIR?${CMAKE_SOURCE_DIR}/src/main/cpp)#?設置dlibinclude(${NATIVE_DIR}/dlib/cmake)#?設置opencv?include文件夾include_directories(${NATIVE_DIR}/opencv/jni/include)#?設置opencv的so庫add_library( ?libopencv_java3 ?SHARED ?IMPORTED) set_target_properties( ?libopencv_java3 ?PROPERTIES ?IMPORTED_LOCATION?${NATIVE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_java3.so)#?將jni_common目錄中所有文件名,存至SRC_LIST中AUX_SOURCE_DIRECTORY(${NATIVE_DIR}/jni_common?SRC_LIST) add_library(?#?Sets?the?name?of?the?library. ?native-lib?#?Sets?the?library?as?a?shared?library. ?SHARED?#?Provides?a?relative?path?to?your?source?file(s). ?${SRC_LIST} ?src/main/cpp/face_detector.h ?src/main/cpp/face_detector.cpp ?src/main/cpp/native-lib.cpp) find_library(?#?Sets?the?name?of?the?path?variable. ?log-lib?#?Specifies?the?name?of?the?NDK?library?that ?#?you?want?CMake?to?locate. ?log) target_link_libraries(?#?Specifies?the?target?library. ?native-lib ?dlib ?libopencv_java3 ?jnigraphics?#?Links?the?target?library?to?the?log?library ?#?included?in?the?NDK. ?${log-lib})#?指定release編譯選項set(CMAKE_C_FLAGS_RELEASE?"${CMAKE_C_FLAGS_RELEASE}?-s?-O3?-Wall")set(CMAKE_CXX_FLAGS_RELEASE?"${CMAKE_CXX_FLAGS_RELEASE}?-s?-O3?-Wall")
由于 C++ 代碼中用到了頭文件 "android/bitmap.h",所以鏈接時需要添加 jnigraphics 庫。
3 JNI相關 Java 類定義
3.1 VisionDetRet 類
VisionDetRet 類的相關對象主要負責 C++ 與 Java 之間的數據傳遞。
public?final?class?VisionDetRet?{?private?int?mLeft;?private?int?mTop;?private?int?mRight;?private?int?mBottom; ?VisionDetRet()?{}?public?VisionDetRet(int?l,?int?t,?int?r,?int?b)?{ ?mLeft?=?l; ?mTop?=?t; ?mRight?=?r; ?mBottom?=?b; ?}?public?int?getLeft()?{?return?mLeft; ?}?public?int?getTop()?{?return?mTop; ?}?public?int?getRight()?{?return?mRight; ?}?public?int?getBottom()?{?return?mBottom; ?} }
3.2 FaceDet 類
FaceDet 類為 JNI 函數調用類,主要定義了一些需要 C++ 實現的 native 方法。
public?class?FaceDet?{?private?static?final?String?TAG?=?"FaceDet";?//?accessed?by?native?methods ?@SuppressWarnings("unused")?private?long?mNativeFaceDetContext;?static?{?try?{?//?預加載native方法庫 ?System.loadLibrary("native-lib"); ?jniNativeClassInit(); ?Log.d(TAG,?"jniNativeClassInit?success"); ?}?catch?(UnsatisfiedLinkError?e)?{ ?Log.e(TAG,?"library?not?found"); ?} ?}?public?FaceDet()?{ ?jniInit(); ?}?@Nullable ?@WorkerThread ?public?List<VisionDetRet>?detect(@NonNull?Bitmap?bitmap)?{ ?VisionDetRet[]?detRets?=?jniBitmapDet(bitmap);?return?Arrays.asList(detRets); ?}?@Override ?protected?void?finalize()?throws?Throwable?{?super.finalize(); ?release(); ?}?public?void?release()?{ ?jniDeInit(); ?}?@Keep ?private?native?static?void?jniNativeClassInit();?@Keep ?private?synchronized?native?int?jniInit();?@Keep ?private?synchronized?native?int?jniDeInit();?@Keep ?private?synchronized?native?VisionDetRet[]?jniBitmapDet(Bitmap?bitmap); }
4 Native 方法實現
4.1 定義 VisionDetRet 類對應的 C++ 類
#include?<jni.h>#define?CLASSNAME_VISION_DET_RET?"com/lightweh/dlib/VisionDetRet"#define?CONSTSIG_VISION_DET_RET?"()V"#define?CLASSNAME_FACE_DET?"com/lightweh/dlib/FaceDet"class?JNI_VisionDetRet?{public: ?JNI_VisionDetRet(JNIEnv?*env)?{?//?查找VisionDetRet類信息 ?jclass?detRetClass?=?env->FindClass(CLASSNAME_VISION_DET_RET);?//?獲取VisionDetRet類成員變量 ?jID_left?=?env->GetFieldID(detRetClass,?"mLeft",?"I"); ?jID_top?=?env->GetFieldID(detRetClass,?"mTop",?"I"); ?jID_right?=?env->GetFieldID(detRetClass,?"mRight",?"I"); ?jID_bottom?=?env->GetFieldID(detRetClass,?"mBottom",?"I"); ?}?void?setRect(JNIEnv?*env,?jobject?&jDetRet,?const?int?&left,?const?int?&top,?const?int?&right,?const?int?&bottom)?{?//?設置VisionDetRet類對象jDetRet的成員變量值 ?env->SetIntField(jDetRet,?jID_left,?left); ?env->SetIntField(jDetRet,?jID_top,?top); ?env->SetIntField(jDetRet,?jID_right,?right); ?env->SetIntField(jDetRet,?jID_bottom,?bottom); ?}?//?創建VisionDetRet類實例 ?static?jobject?createJObject(JNIEnv?*env)?{ ?jclass?detRetClass?=?env->FindClass(CLASSNAME_VISION_DET_RET); ?jmethodID?mid?= ?env->GetMethodID(detRetClass,?"<init>",?CONSTSIG_VISION_DET_RET);?return?env->NewObject(detRetClass,?mid); ?}?//?創建VisionDetRet類對象數組 ?static?jobjectArray?createJObjectArray(JNIEnv?*env,?const?int?&size)?{ ?jclass?detRetClass?=?env->FindClass(CLASSNAME_VISION_DET_RET);?return?(jobjectArray)?env->NewObjectArray(size,?detRetClass,?NULL); ?}private: ?jfieldID?jID_left; ?jfieldID?jID_top; ?jfieldID?jID_right; ?jfieldID?jID_bottom; };
4.2 定義人臉檢測類
人臉檢測算法需要用大小位置不同的窗口在圖像中進行滑動,然后判斷窗口中是否存在人臉。本文采用的是 dlib 中的是HOG(histogram of oriented gradient)方法對人臉進行檢測,其檢測效果要好于 opencv。dlib 中同樣提供了 CNN 方法來進行人臉檢測,效果好于 HOG,不過需要使用 GPU 加速,不然程序運行會非常慢。
class?FaceDetector?{private: ?dlib::frontal_face_detector?face_detector;?std::vector<dlib::rectangle>?det_rects;public: ?FaceDetector();?//?實現人臉檢測算法 ?int?Detect(const?cv::Mat?&image);?//?返回檢測結果 ?std::vector<dlib::rectangle>?getDetResultRects(); }; FaceDetector::FaceDetector()?{?//?定義人臉檢測器 ?face_detector?=?dlib::get_frontal_face_detector(); }int?FaceDetector::Detect(const?cv::Mat?&image)?{?if?(image.empty())?return?0;?if?(image.channels()?==?1)?{ ?cv::cvtColor(image,?image,?CV_GRAY2BGR); ?} ?dlib::cv_image<dlib::bgr_pixel>?dlib_image(image); ?det_rects.clear();?//?返回檢測到的人臉矩形特征框 ?det_rects?=?face_detector(dlib_image);?return?det_rects.size(); }std::vector<dlib::rectangle>?FaceDetector::getDetResultRects()?{?return?det_rects; }
4.3 native 方法實現
JNI_VisionDetRet?*g_pJNI_VisionDetRet; JavaVM?*g_javaVM?=?NULL;//?該函數在加載本地庫時被調用JNIEXPORT?jint?JNI_OnLoad(JavaVM?*vm,?void?*reserved)?{ ?g_javaVM?=?vm; ?JNIEnv?*env; ?vm->GetEnv((void?**)?&env,?JNI_VERSION_1_6);?//?初始化?g_pJNI_VisionDetRet ?g_pJNI_VisionDetRet?=?new?JNI_VisionDetRet(env);?return?JNI_VERSION_1_6; }//?該函數用于執行清理操作void?JNI_OnUnload(JavaVM?*vm,?void?*reserved)?{ ?g_javaVM?=?NULL;?delete?g_pJNI_VisionDetRet; }namespace?{#define?JAVA_NULL?0 ?using?DetPtr?=?FaceDetector?*;?//?用于存放人臉檢測類對象的指針,關聯Jave層對象與C++底層對象(相互對應) ?class?JNI_FaceDet?{ ?public: ?JNI_FaceDet(JNIEnv?*env)?{ ?jclass?clazz?=?env->FindClass(CLASSNAME_FACE_DET); ?mNativeContext?=?env->GetFieldID(clazz,?"mNativeFaceDetContext",?"J"); ?env->DeleteLocalRef(clazz); ?}?DetPtr?getDetectorPtrFromJava(JNIEnv?*env,?jobject?thiz)?{ ?DetPtr?const?p?=?(DetPtr)?env->GetLongField(thiz,?mNativeContext);?return?p; ?}?void?setDetectorPtrToJava(JNIEnv?*env,?jobject?thiz,?jlong?ptr)?{ ?env->SetLongField(thiz,?mNativeContext,?ptr); ?} ?jfieldID?mNativeContext; ?};?//?Protect?getting/setting?and?creating/deleting?pointer?between?java/native ?std::mutex?gLock;?std::shared_ptr<JNI_FaceDet>?getJNI_FaceDet(JNIEnv?*env)?{?static?std::once_flag?sOnceInitflag;?static?std::shared_ptr<JNI_FaceDet>?sJNI_FaceDet;?std::call_once(sOnceInitflag,?[env]()?{ ?sJNI_FaceDet?=?std::make_shared<JNI_FaceDet>(env); ?});?return?sJNI_FaceDet; ?}?//?從java對象獲取它持有的c++對象指針 ?DetPtr?const?getDetPtr(JNIEnv?*env,?jobject?thiz)?{?std::lock_guard<std::mutex>?lock(gLock);?return?getJNI_FaceDet(env)->getDetectorPtrFromJava(env,?thiz); ?}?//?The?function?to?set?a?pointer?to?java?and?delete?it?if?newPtr?is?empty ?//?C++對象new以后,將指針轉成long型返回給java對象持有 ?void?setDetPtr(JNIEnv?*env,?jobject?thiz,?DetPtr?newPtr)?{?std::lock_guard<std::mutex>?lock(gLock); ?DetPtr?oldPtr?=?getJNI_FaceDet(env)->getDetectorPtrFromJava(env,?thiz);?if?(oldPtr?!=?JAVA_NULL)?{?delete?oldPtr; ?} ?getJNI_FaceDet(env)->setDetectorPtrToJava(env,?thiz,?(jlong)?newPtr); ?} }?//?end?unnamespace#ifdef?__cplusplusextern?"C"?{#endif#define?DLIB_FACE_JNI_METHOD(METHOD_NAME)?Java_com_lightweh_dlib_FaceDet_##METHOD_NAMEvoid?JNIEXPORTDLIB_FACE_JNI_METHOD(jniNativeClassInit)(JNIEnv?*env,?jclass?_this)?{}//?生成需要返回的結果數組jobjectArray?getRecResult(JNIEnv?*env,?DetPtr?faceDetector,?const?int?&size)?{?//?根據檢測到的人臉數創建相應大小的jobjectArray ?jobjectArray?jDetRetArray?=?JNI_VisionDetRet::createJObjectArray(env,?size);?for?(int?i?=?0;?i?<?size;?i++)?{?//?對檢測到的每一個人臉創建對應的實例對象,然后插入數組 ?jobject?jDetRet?=?JNI_VisionDetRet::createJObject(env); ?env->SetObjectArrayElement(jDetRetArray,?i,?jDetRet); ?dlib::rectangle?rect?=?faceDetector->getDetResultRects()[i];?//?將人臉矩形框的值賦給對應的jobject實例對象 ?g_pJNI_VisionDetRet->setRect(env,?jDetRet,?rect.left(),?rect.top(), ?rect.right(),?rect.bottom()); ?}?return?jDetRetArray; }JNIEXPORT?jobjectArray?JNICALLDLIB_FACE_JNI_METHOD(jniBitmapDet)(JNIEnv?*env,?jobject?thiz,?jobject?bitmap)?{ ?cv::Mat?rgbaMat; ?cv::Mat?bgrMat; ?jniutils::ConvertBitmapToRGBAMat(env,?bitmap,?rgbaMat,?true); ?cv::cvtColor(rgbaMat,?bgrMat,?cv::COLOR_RGBA2BGR);?//?獲取人臉檢測類指針 ?DetPtr?mDetPtr?=?getDetPtr(env,?thiz);?//?調用人臉檢測算法,返回檢測到的人臉數 ?jint?size?=?mDetPtr->Detect(bgrMat);?//?返回檢測結果 ?return?getRecResult(env,?mDetPtr,?size); }jint?JNIEXPORT?JNICALLDLIB_FACE_JNI_METHOD(jniInit)(JNIEnv?*env,?jobject?thiz)?{ ?DetPtr?mDetPtr?=?new?FaceDetector();?//?設置人臉檢測類指針 ?setDetPtr(env,?thiz,?mDetPtr);?return?JNI_OK; }jint?JNIEXPORT?JNICALLDLIB_FACE_JNI_METHOD(jniDeInit)(JNIEnv?*env,?jobject?thiz)?{?//?指針置0 ?setDetPtr(env,?thiz,?JAVA_NULL);?return?JNI_OK; }#ifdef?__cplusplus}#endif
5 Java端調用人臉檢測算法
在開啟人臉檢測之前,需要在相機 AutoFitTextureView 上覆蓋一層自定義 BoundingBoxView 用于繪制檢測到的人臉矩形框,該 View 的具體實現如下:
public?class?BoundingBoxView?extends?SurfaceView?implements?SurfaceHolder.Callback?{?protected?SurfaceHolder?mSurfaceHolder;?private?Paint?mPaint;?private?boolean?mIsCreated;?public?BoundingBoxView(Context?context,?AttributeSet?attrs)?{?super(context,?attrs); ?mSurfaceHolder?=?getHolder(); ?mSurfaceHolder.addCallback(this); ?mSurfaceHolder.setFormat(PixelFormat.TRANSPARENT); ?setZOrderOnTop(true); ?mPaint?=?new?Paint(); ?mPaint.setAntiAlias(true); ?mPaint.setColor(Color.RED); ?mPaint.setStrokeWidth(5f); ?mPaint.setStyle(Paint.Style.STROKE); ?}?@Override ?public?void?surfaceChanged(SurfaceHolder?surfaceHolder,?int?format,?int?width,?int?height)?{ ?}?@Override ?public?void?surfaceCreated(SurfaceHolder?surfaceHolder)?{ ?mIsCreated?=?true; ?}?@Override ?public?void?surfaceDestroyed(SurfaceHolder?surfaceHolder)?{ ?mIsCreated?=?false; ?}?public?void?setResults(List<VisionDetRet>?detRets) ?{?if?(!mIsCreated)?{?return; ?} ?Canvas?canvas?=?mSurfaceHolder.lockCanvas();?//清除掉上一次的畫框。 ?canvas.drawColor(Color.TRANSPARENT,?PorterDuff.Mode.CLEAR); ?canvas.drawColor(Color.TRANSPARENT);?for?(VisionDetRet?detRet?:?detRets)?{ ?Rect?rect?=?new?Rect(detRet.getLeft(),?detRet.getTop(),?detRet.getRight(),?detRet.getBottom()); ?canvas.drawRect(rect,?mPaint); ?} ?mSurfaceHolder.unlockCanvasAndPost(canvas); ?} }
同時,需要在布局文件中添加對應的 BoundingBoxView 層,保證與 AutoFitTextureView 完全重合:
<?xml?version="1.0"?encoding="utf-8"?><RelativeLayout?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=".CameraFragment"> ?<com.lightweh.facedetection.AutoFitTextureView ?android:id="@+id/textureView" ?android:layout_width="wrap_content" ?android:layout_height="wrap_content" ?android:layout_centerVertical="true" ?android:layout_centerHorizontal="true"?/> ?<com.lightweh.facedetection.BoundingBoxView ?android:id="@+id/boundingBoxView" ?android:layout_width="wrap_content" ?android:layout_height="wrap_content" ?android:layout_alignLeft="@+id/textureView" ?android:layout_alignTop="@+id/textureView" ?android:layout_alignRight="@+id/textureView" ?android:layout_alignBottom="@+id/textureView"?/></RelativeLayout>
BoundingBoxView 添加完成以后,即可在 CameraFragment 中添加對應的人臉檢測代碼:
private?class?detectAsync?extends?AsyncTask<Bitmap,?Void,?List<VisionDetRet>>?{?@Override ?protected?void?onPreExecute()?{ ?mIsDetecting?=?true;?super.onPreExecute(); ?}?protected?List<VisionDetRet>?doInBackground(Bitmap...?bp)?{ ?List<VisionDetRet>?results;?//?返回檢測結果 ?results?=?mFaceDet.detect(bp[0]);?return?results; ?}?protected?void?onPostExecute(List<VisionDetRet>?results)?{?//?繪制檢測到的人臉矩形框 ?mBoundingBoxView.setResults(results); ?mIsDetecting?=?false; ?} }
然后,分別在 onResume 與 onPause 函數中完成人臉檢測類對象的初始化和釋放:
@Overridepublic?void?onResume()?{?super.onResume(); ?startBackgroundThread(); ?mFaceDet?=?new?FaceDet();?if?(mTextureView.isAvailable())?{ ?openCamera(mTextureView.getWidth(),?mTextureView.getHeight()); ?}?else?{ ?mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); ?} }@Overridepublic?void?onPause()?{ ?closeCamera(); ?stopBackgroundThread();?if?(mFaceDet?!=?null)?{ ?mFaceDet.release(); ?}?super.onPause(); }
最后,在 TextureView 的回調函數 onSurfaceTextureUpdated 完成調用:
@Overridepublic?void?onSurfaceTextureUpdated(SurfaceTexture?texture)?{?if?(!mIsDetecting)?{ ?Bitmap?bp?=?mTextureView.getBitmap();?//?保證圖片方向與預覽方向一致 ?bp?=?Bitmap.createBitmap(bp,?0,?0,?bp.getWidth(),?bp.getHeight(),?mTextureView.getTransform(null),?true?);?new?detectAsync().execute(bp); ?} }
6 測試結果
經測試,960x720的 bitmap 圖片在華為手機(Android 6.0,8核1.2GHz,2G內存)上執行一次檢測約耗時800~850ms。Demo 運行效果如下:
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。