openCV

(스크랩 ) OpenCV와 NDK를 사용하여 Android에서 Face Detection(얼굴 인식)

Machine_웅 2018. 6. 28. 18:31
728x90
반응형

OpenCV 배포시 포함되어 있는 얼굴 인식 C++코드를 NDK를 이용하여 Android에서 동작하도록 수정하였습니다.  


안드로이드 +  NDK 카메라 기본코드에 단순히 C++코드만 옮겨오면 되는 줄 알았는데 고려해야 하는게 생각보다 많군요..


사용한 C++ 코드는 다음 위치에서 가져왔습니다.

https://github.com/opencv/opencv/blob/master/samples/cpp/tutorial_code/objectDetection/objectDetection.cpp




다음 순서로 설명합니다.


1. 프로그램 흐름 및 실행결과


2. 코드 수정 및 사용방법


3. 캡쳐 버튼 추가하기


4. 인식된 얼굴 갯수 반환받기



       최초 작성 - 2016. 12. 9

                          2018. 5. 26 NDK 17 버전 대로 업그레이드 후 CMake 방식에서 발생하는 문제 수정

                          2018. 5. 27 ndk-build 방식 바뀐 부분 수정

                          2018. 5. 30 CMake 방식 포스팅 오류 수정

                          2018. 6. 19 캡쳐버튼 추가

                          2018. 6. 21 cpp 코드에서 자바코드로 인식된 얼굴 개수 리턴받는 코드 추가



1. 프로그램 흐름 및 실행결과

1-1. Android 프로젝트의 assets 폴더에 넣었던 XML 파일(Harr cascade 트레이닝 데이터)는 프로젝트 deploy시 같이 폰에 올라가지만 읽어오기 위해서는 안드로이 폰의 내부 저장소(Internal Storage)로 옮기는 작업이 필요합니다. (JAVA read_cascade_file)


   private void read_cascade_file(){

       //copyFile 메소드 Assets에서 해당 파일을 가져와

       //외부 저장소 특정위치에 저장하도록 구현된 메소드입니다.
       copyFile("haarcascade_frontalface_alt.xml");
       copyFile("haarcascade_eye_tree_eyeglasses.xml");

       Log.d(TAG, "read_cascade_file:");


       //loadCascade 메소드는 외부 저장소의 특정 위치에서 해당 파일을 읽어와서

       //CascadeClassifier 객체로 로드합니다.
       cascadeClassifier_face = loadCascade( "haarcascade_frontalface_alt.xml");
       Log.d(TAG, "read_cascade_file:");

       cascadeClassifier_eye = loadCascade( "haarcascade_eye_tree_eyeglasses.xml");
   }




1-2. 내부 저장소로부터 XML 파일(Harr cascade 트레이닝 데이터)을 읽어와 CascadeClassifier 객체를 생성후 자바로 넘겨줍니다.(C++ loadCascade)




1-3. 카메라로부터 영상을 읽어올 때  전면 카메라의 경우 영상이 뒤집혀서 읽히기 때문에  180도 회전 시켜줘야 합니다.(JAVA onCameraFrame)

   @Override
   public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {

       matInput = inputFrame.rgba();

       if ( matResult != null ) matResult.release();
       matResult = new Mat(matInput.rows(), matInput.cols(), matInput.type());

       //ConvertRGBtoGray(matInput.getNativeObjAddr(), matResult.getNativeObjAddr());
       Core.flip(matInput, matInput, 1);

       detect(cascadeClassifier_face,cascadeClassifier_eye, matInput.getNativeObjAddr(),
               matResult.getNativeObjAddr());

       return matResult;
   }




1-4. JAVA에서 영상이 들어오기 시작하면 CascadeClassifier 객체를 인자로 해서 호출되어 얼굴 인식 결과를 영상에 표시해줍니다. (C++ detect)




1-5. 최종 결과를 안드로이드폰의 화면에 보여지도록 결과 Mat 객체를 리턴합니다.(JAVA onCameraFrame)


   @Override
   public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {

       matInput = inputFrame.rgba();

       if ( matResult != null ) matResult.release();
       matResult = new Mat(matInput.rows(), matInput.cols(), matInput.type());

       //ConvertRGBtoGray(matInput.getNativeObjAddr(), matResult.getNativeObjAddr());
       Core.flip(matInput, matInput, 1);

       detect(cascadeClassifier_face,cascadeClassifier_eye, matInput.getNativeObjAddr(),
               matResult.getNativeObjAddr());

       return matResult;
   }




실행결과입니다. 얼굴 위치와 눈 위치가 검출된 결과입니다.  

이상하게도 화면이 portrait 방향일 때에는 검출이 안되고  아래 화면처럼 landscape 방향일 때에만 검출이 됩니다.

단 가로로 회전시켜야 얼굴 인식이 됩니다.

(OpenCV에서 전체 화면을 사용하기 위해서 가로로 화면을 고정시켜 놓았기때문입니다.)


주의 할 점은 이미지 프로세싱 혹은 컴퓨터 비전 기술 특성상 장소에 따라 제대로 인식이 안되거나 오류가 있을 수 있습니다.

가장 큰 원인은 장소마다 조명 상태가 다르기 때문입니다.  

적절한 조명 상태에서 잘 인식이 됩니다.  


추후 시간나는대로 코드분석 결과를 포스팅해서 본 포스팅에 링크를 걸어둘 예정입니다.

(아직 미정입니다.)




Genymotion에서는 웹캠을 이용하여 얼굴 인식 앱의 테스트가 가능합니다.

스마트폰보다는 느리지만 테스트할만합니다.


Genymotion 설치 및 Android Studio와 연동하여 사용하는 방법

http://webnautes.tistory.com/1145




2. 코드 수정 및 사용방법


2-1.  다음 포스팅 중 하나를 참고하여 OpenCV 지원하는 안드로이드 프로젝트를 생성합니다.

참고로 현재 Android Studio에서 공식 지원하는 방법은 CMake 사용하는 방법입니다.


Android NDK + OpenCV 카메라 예제 및 프로젝트 생성방법(ndk-build 사용)

http://webnautes.tistory.com/923


Android NDK + OpenCV 카메라 예제 및 프로젝트 생성방법(CMake 사용)

http://webnautes.tistory.com/1054




2-2. 프로젝트 패널에서 app / src / main을 선택한 상태에서 마우스 우클릭하여 보이는 메뉴에서 New > Directory를 선택합니다.




assets라고 입력하고 OK 버튼을 누르면 assets 이름의 디렉토리가 생성됩니다.






2-3. 아래 링크를 각각 클릭하여 해당 페이지로 이동하면, Ctrl + S를 눌러서 파일을 저장합니다.

Twibap 님이 알려주신 방법입니다.


https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_eye_tree_eyeglasses.xml


https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_alt.xml

 

 

 

( * 메모장을 열어서 전체를 복사한후에 xml 파일 형태로 저장하자 )



프로젝트 패널의 assets 디렉토리에 파일을 드래그 앤 드롭 하거나 assets 디렉토리를 선택한 상태에서 마우스 우클릭하여 보이는 메뉴에서 Show in Explorer를 선택한 후, 탐색기에서 해당 파일을 복사해줍니다.





2-4. AndroidManifest.xml 파일에 외부 저장소 접근 권한을 추가해줍니다.


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

   uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
   <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"/>




2-5. ndk-build 사용하도록 프로젝트 생성했다면 다음처럼 android 라이브러리를 사용하도록 Android.mk 파일을 수정합니다.


LOCAL_MODULE    := native-lib
LOCAL_SRC_FILES := main.cpp
LOCAL_LDLIBS += -llog -landroid

include $(BUILD_SHARED_LIBRARY)



CMake 사용하도록 프로젝트 생성했다면 다음처럼 android 라이브러리를 사용하도록  CMakeLists.txt 파일을 수정합니다.


find_library( android-lib android)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                      native-lib

                      ${android-lib}

                      lib_opencv

                      # Links the target library to the log library
                      # included in the NDK.
                      ${log-lib} )




2-6.  자바 코드 수정합니다.


수정 1. 기존 코드(흰색)를 주석 처리하고 외장 저장소에 파일 저장하기 위한 퍼미션(노란색)을 추가합니다.


   // String[] PERMISSIONS  = {"android.permission.CAMERA"};
   String[] PERMISSIONS  = {"android.permission.CAMERA",
           "android.permission.WRITE_EXTERNAL_STORAGE"};



수정 2.   기존 코드(흰색)를 주석처리하고 WRITE_EXTERNAL_STORAGE 퍼미션을 위해 필요한 코드(노란색)를 추가합니다.


   @Override
   public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                          @NonNull int[] grantResults) {

       super.onRequestPermissionsResult(requestCode, permissions, grantResults);

       switch(requestCode){

           case PERMISSIONS_REQUEST_CODE:
               if (grantResults.length > 0) {
                   boolean cameraPermissionAccepted = grantResults[0]
                           == PackageManager.PERMISSION_GRANTED;

                   // 기존 코드 주석처리

                   // if (!cameraPermissionAccepted)

                   // showDialogForPermission("앱을 실행하려면 퍼미션을 허가하셔야합니다.");


                   boolean writePermissionAccepted = grantResults[1]
                           == PackageManager.PERMISSION_GRANTED;

                   if (!cameraPermissionAccepted || !writePermissionAccepted) {
                       showDialogForPermission("앱을 실행하려면 퍼미션을 허가하셔야합니다.");
                       return;
                   }else
                   {
                       read_cascade_file();
                   }
               }
               break;
       }

   }



수정 3.  기존 코드(흰색)를 주석처리하고 퍼미션이 이미 허가된 경우의 처리를 위한 코드(노란색)를 추가하고 사용하는 카메라를 전면 카메라로 바꾸기 위한 코드를 수정합니다.


       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
           //퍼미션 상태 확인
           if (!hasPermissions(PERMISSIONS)) {

               //퍼미션 허가 안되어있다면 사용자에게 요청
               requestPermissions(PERMISSIONS, PERMISSIONS_REQUEST_CODE);
           }
           else  read_cascade_file(); //추가
       }

       else read_cascade_file(); //추가

       mOpenCvCameraView = (CameraBridgeViewBase)findViewById(R.id.activity_surface_view);
       mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);
       mOpenCvCameraView.setCvCameraViewListener(this);

       // 기존 코드 주석처리
       //mOpenCvCameraView.setCameraIndex(0); // front-camera(1),  back-camera(0)
       mOpenCvCameraView.setCameraIndex(1); // front-camera(1),  back-camera(0)

       mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);



수정 4.  기존 코드(흰색)를 주석처리하고 cpp에 추가할 jni함수를 위한 네이티브 메소드 선언(노란색)을 추가합니다.

   //public native void ConvertRGBtoGray(long matAddrInput, long matAddrResult);
   public native long loadCascade(String cascadeFileName );
   public native void detect(long cascadeClassifier_face,

       long cascadeClassifier_eye, long matAddrInput, long matAddrResult);
   public long cascadeClassifier_face = 0;
   public long cascadeClassifier_eye = 0;



수정 5.  xml 파일을 가져오기 위한 메소드(노란색)를 추가합니다.


  • cpp 파일의 loadCascade 함수를 호출하도록 구현되어있는데 자바 함수를 사용하도록 변경해도 됩니다.

  • 현재 이미 파일을 copyFile 메소드를 이용해서 가져온 경우에 대한 처리가 빠져있습니다.  


  static {

       System.loadLibrary("opencv_java3");

       System.loadLibrary("native-lib");

  }

  private void copyFile(String filename) {
       String baseDir = Environment.getExternalStorageDirectory().getPath();
       String pathDir = baseDir + File.separator + filename;

       AssetManager assetManager = this.getAssets();

       InputStream inputStream = null;
       OutputStream outputStream = null;

       try {
           Log.d( TAG, "copyFile :: 다음 경로로 파일복사 "+ pathDir);
           inputStream = assetManager.open(filename);
           outputStream = new FileOutputStream(pathDir);

           byte[] buffer = new byte[1024];
           int read;
           while ((read = inputStream.read(buffer)) != -1) {
               outputStream.write(buffer, 0, read);
           }
           inputStream.close();
           inputStream = null;
           outputStream.flush();
           outputStream.close();
           outputStream = null;
       } catch (Exception e) {
           Log.d(TAG, "copyFile :: 파일 복사 중 예외 발생 "+e.toString() );
       }

   }

   private void read_cascade_file(){
       copyFile("haarcascade_frontalface_alt.xml");
       copyFile("haarcascade_eye_tree_eyeglasses.xml");

       Log.d(TAG, "read_cascade_file:");

       cascadeClassifier_face = loadCascade( "haarcascade_frontalface_alt.xml");
       Log.d(TAG, "read_cascade_file:");

       cascadeClassifier_eye = loadCascade( "haarcascade_eye_tree_eyeglasses.xml");
   }


   private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {

       @Override

       public void onManagerConnected(int status) {



추가한 코드에서 빨간선이 보이는 부분에서 Alt + Enter를 눌러서 필요한 패키지를 임포트 하도록합니다.




수정 6. 기존 코드(흰색)를 주석처리하고 얼굴 검출하는 cpp 코드를 호출하는 코드(노란색)을 추가합니다.


카메라로부터 영상을 가져올 때마다 호출되는 onCameraFrame 메소드에서  jni 함수 detect를 호출하도록 합니다.

추가한 코드에서 빨간선이 보이는 부분에서 Alt + Enter를 눌러서 필요한 패키지를 임포트 하도록합니다.


   @Override
   public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {

       matInput = inputFrame.rgba();

       if ( matResult != null ) matResult.release();
       matResult = new Mat(matInput.rows(), matInput.cols(), matInput.type());

       // 기존 코드 주석처리
       // ConvertRGBtoGray(matInput.getNativeObjAddr(), matResult.getNativeObjAddr());
       Core.flip(matInput, matInput, 1);

       detect(cascadeClassifier_face,cascadeClassifier_eye, matInput.getNativeObjAddr(),
               matResult.getNativeObjAddr());

       return matResult;
   }




2-7.   진행한 방식에 따라 하는 방법이 다릅니다.


CMake 방식이라면..


자바 코드에서 loadCascade 메소드에 각각  마우스 커서를 가져가면 보이는 빨간 전구를 클릭한 후, 보이는 메뉴에서  Create Function을 클릭합니다.




자바 코드의 loadCascade 메소드에 대응되는 cpp 함수가 생성된 후, 자바 코드에서 loadCascade 함수는 검은색으로 변합니다.

detect 메소드도 같은 방식으로 진행합니다.




두 메소드 이름이 모두 검은색으로 보여야합니다.





ndk-build 방식이라면..


자바코드 파일 MainActivity를 선택하고 마우스 우클릭하여 보이는 메뉴에서 External Tools > javah를 선택합니다.




문제 없이 진행되면 기존 com_tistory_webnautes_useopencvwithndk_build_MainActivity.h 파일에 덮어쓰기 되었다는 메시지가 보입니다.


"C:\Program Files\Android\Android Studio\jre\bin\javah.exe" -v -jni -d C:\Users\webnautes\AndroidStudioProjects\UseOpenCVwithndkbuild\app/src/main/jni com.tistory.webnautes.useopencvwithndk_build.MainActivity
Error: unmappable character for encoding MS949
.........................................................................
Error: unmappable character for encoding MS949
[Overwriting file RegularFileObject[C:\Users\webnautes\AndroidStudioProjects\UseOpenCVwithndkbuild\app\src\main\jni\com_tistory_webnautes_useopencvwithndk_build_MainActivity.h]]

Process finished with exit code 0



ndk-build 사용하는 경우 8번을 진행합니다.

CMake 사용하는 경우 9번, 10번을 차례대로 진행합니다.




2-8.  com_tistory_webnautes_useopencvwithndk_build_MainActivity.h 파일을 열어보면 기존 함수 선언은 제거되고 자바 코드에 새로 추가된 네이티브 메소드 선언을 위한 함수 선언이 추가되어 있습니다.


/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_tistory_webnautes_useopencvwithndk_build_MainActivity */

#ifndef _Included_com_tistory_webnautes_useopencvwithndk_build_MainActivity
#define _Included_com_tistory_webnautes_useopencvwithndk_build_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
#undef com_tistory_webnautes_useopencvwithndk_build_MainActivity_PERMISSIONS_REQUEST_CODE
#define com_tistory_webnautes_useopencvwithndk_build_MainActivity_PERMISSIONS_REQUEST_CODE 1000L
/*
* Class:     com_tistory_webnautes_useopencvwithndk_build_MainActivity
* Method:    loadCascade
* Signature: (Ljava/lang/String;)J
*/
JNIEXPORT jlong JNICALL Java_com_tistory_webnautes_useopencvwithndk_1build_MainActivity_loadCascade
 (JNIEnv *, jobject, jstring);

/*
* Class:     com_tistory_webnautes_useopencvwithndk_build_MainActivity
* Method:    detect
* Signature: (JJJJ)V
*/
JNIEXPORT void JNICALL Java_com_tistory_webnautes_useopencvwithndk_1build_MainActivity_detect
 (JNIEnv *, jobject, jlong, jlong, jlong, jlong);

#ifdef __cplusplus
}
#endif
#endif




기존 코드를 주석처리(흰색)하고  com_tistory_webnautes_useopencvwithndk_build_MainActivity.h 파일에서 함수 선언(노란색)을 복사해옵니다.

추가로 필요한 코드(파란색)을 추가합니다.


이제 안드로이드 디바이스에 설치해보면 됩니다.


#include <jni.h>
#include "com_tistory_webnautes_useopencvwithndk_build_MainActivity.h"
#include <opencv2/opencv.hpp>
#include <android/log.h>


using namespace cv;
using namespace std;

extern "C"{

/*
   JNIEXPORT void JNICALL
  Java_com_tistory_webnautes_useopencvwithndk_1build_MainActivity_ConvertRGBtoGray(
           JNIEnv *env,
           jobject instance,
           jlong matAddrInput,
           jlong matAddrResult){


       Mat &matInput = *(Mat *)matAddrInput;
       Mat &matResult = *(Mat *)matAddrResult;

       cvtColor(matInput, matResult, CV_RGBA2GRAY);


     }
*/


   float resize(Mat img_src, Mat &img_resize, int resize_width){

       float scale = resize_width / (float)img_src.cols ;
       if (img_src.cols > resize_width) {
           int new_height = cvRound(img_src.rows * scale);
           resize(img_src, img_resize, Size(resize_width, new_height));
       }
       else {
           img_resize = img_src;
       }
       return scale;
   }


   JNIEXPORT jlong JNICALL Java_com_tistory_webnautes_useopencvwithndk_1build_MainActivity_loadCascade
           (JNIEnv *env, jobject type, jstring cascadeFileName_){

       const char *nativeFileNameString = env->GetStringUTFChars(cascadeFileName_, 0);

       string baseDir("/storage/emulated/0/");
       baseDir.append(nativeFileNameString);
       const char *pathDir = baseDir.c_str();

       jlong ret = 0;
       ret = (jlong) new CascadeClassifier(pathDir);
       if (((CascadeClassifier *) ret)->empty()) {
           __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ",
                               "CascadeClassifier로 로딩 실패 %s", nativeFileNameString);
       }
       else
           __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ",
                               "CascadeClassifier로 로딩 성공 %s", nativeFileNameString);


       env->ReleaseStringUTFChars(cascadeFileName_, nativeFileNameString);

       return ret;
   }



   JNIEXPORT void JNICALL Java_com_tistory_webnautes_useopencvwithndk_1build_MainActivity_detect
           (JNIEnv *env, jobject type, jlong cascadeClassifier_face, jlong cascadeClassifier_eye, jlong matAddrInput, jlong matAddrResult){

       Mat &img_input = *(Mat *) matAddrInput;
       Mat &img_result = *(Mat *) matAddrResult;

       img_result = img_input.clone();

       std::vector<Rect> faces;
       Mat img_gray;

       cvtColor(img_input, img_gray, COLOR_BGR2GRAY);
       equalizeHist(img_gray, img_gray);

       Mat img_resize;
       float resizeRatio = resize(img_gray, img_resize, 640);

       //-- Detect faces
       ((CascadeClassifier *) cascadeClassifier_face)->detectMultiScale( img_resize, faces, 1.1, 2, 0|CASCADE_SCALE_IMAGE, Size(30, 30) );


       __android_log_print(ANDROID_LOG_DEBUG, (char *) "native-lib :: ",
                           (char *) "face %d found ", faces.size());

       for (int i = 0; i < faces.size(); i++) {
           double real_facesize_x = faces[i].x / resizeRatio;
           double real_facesize_y = faces[i].y / resizeRatio;
           double real_facesize_width = faces[i].width / resizeRatio;
           double real_facesize_height = faces[i].height / resizeRatio;

           Point center( real_facesize_x + real_facesize_width / 2, real_facesize_y + real_facesize_height/2);
           ellipse(img_result, center, Size( real_facesize_width / 2, real_facesize_height / 2), 0, 0, 360,
                   Scalar(255, 0, 255), 30, 8, 0);


           Rect face_area(real_facesize_x, real_facesize_y, real_facesize_width,real_facesize_height);
           Mat faceROI = img_gray( face_area );
           std::vector<Rect> eyes;

           //-- In each face, detect eyes
           ((CascadeClassifier *) cascadeClassifier_eye)->detectMultiScale( faceROI, eyes, 1.1, 2, 0 |CASCADE_SCALE_IMAGE, Size(30, 30) );

           for ( size_t j = 0; j < eyes.size(); j++ )
           {
               Point eye_center( real_facesize_x + eyes[j].x + eyes[j].width/2, real_facesize_y + eyes[j].y + eyes[j].height/2 );
               int radius = cvRound( (eyes[j].width + eyes[j].height)*0.25 );
               circle( img_result, eye_center, radius, Scalar( 255, 0, 0 ), 30, 8, 0 );
           }
       }


   }
}




2-9. native-lib.cpp 파일을 다음처럼 수정합니다..

기존 코드를 주석처리(흰색)하고 필요한 코드(파란색)을 추가합니다.


#include <jni.h>

#include <opencv2/opencv.hpp>

#include <android/log.h>


using namespace cv;

using namespace std;



//extern "C"

//JNIEXPORT void JNICALL

//Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_ConvertRGBtoGray(JNIEnv *env,

//                                                                            jobject instance,

//                                                                            jlong matAddrInput,

//                                                                            jlong matAddrResult) {

//

//    Mat &matInput = *(Mat *)matAddrInput;

//    Mat &matResult = *(Mat *)matAddrResult;

//

//    cvtColor(matInput, matResult, CV_RGBA2GRAY);

//

//}




float resize(Mat img_src, Mat &img_resize, int resize_width){


   float scale = resize_width / (float)img_src.cols ;

   if (img_src.cols > resize_width) {

       int new_height = cvRound(img_src.rows * scale);

       resize(img_src, img_resize, Size(resize_width, new_height));

   }

   else {

       img_resize = img_src;

   }

   return scale;

}



extern "C"

JNIEXPORT jlong JNICALL

Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_loadCascade(JNIEnv *env,

                                                                      jobject instance,

                                                                      jstring cascadeFileName_) {

//    const char *cascadeFileName = env->GetStringUTFChars(cascadeFileName_, 0);


   const char *nativeFileNameString = env->GetStringUTFChars(cascadeFileName_, 0);


   string baseDir("/storage/emulated/0/");

   baseDir.append(nativeFileNameString);

   const char *pathDir = baseDir.c_str();


   jlong ret = 0;

   ret = (jlong) new CascadeClassifier(pathDir);

   if (((CascadeClassifier *) ret)->empty()) {

       __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ",

                           "CascadeClassifier로 로딩 실패 %s", nativeFileNameString);

   }

   else

       __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ",

                           "CascadeClassifier로 로딩 성공 %s", nativeFileNameString);



   env->ReleaseStringUTFChars(cascadeFileName_, nativeFileNameString);


   return ret;


//    env->ReleaseStringUTFChars(cascadeFileName_, cascadeFileName);

}



extern "C"

JNIEXPORT void JNICALL

Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_detect(JNIEnv *env, jobject instance,

                                                                 jlong cascadeClassifier_face,

                                                                 jlong cascadeClassifier_eye,

                                                                 jlong matAddrInput,

                                                                 jlong matAddrResult) {


   Mat &img_input = *(Mat *) matAddrInput;

   Mat &img_result = *(Mat *) matAddrResult;


   img_result = img_input.clone();


   std::vector<Rect> faces;

   Mat img_gray;


   cvtColor(img_input, img_gray, COLOR_BGR2GRAY);

   equalizeHist(img_gray, img_gray);


   Mat img_resize;

   float resizeRatio = resize(img_gray, img_resize, 640);


   //-- Detect faces

   ((CascadeClassifier *) cascadeClassifier_face)->detectMultiScale( img_resize, faces, 1.1, 2, 0|CASCADE_SCALE_IMAGE, Size(30, 30) );



   __android_log_print(ANDROID_LOG_DEBUG, (char *) "native-lib :: ",

                       (char *) "face %d found ", faces.size());


   for (int i = 0; i < faces.size(); i++) {

       double real_facesize_x = faces[i].x / resizeRatio;

       double real_facesize_y = faces[i].y / resizeRatio;

       double real_facesize_width = faces[i].width / resizeRatio;

       double real_facesize_height = faces[i].height / resizeRatio;


       Point center( real_facesize_x + real_facesize_width / 2, real_facesize_y + real_facesize_height/2);

       ellipse(img_result, center, Size( real_facesize_width / 2, real_facesize_height / 2), 0, 0, 360,

               Scalar(255, 0, 255), 30, 8, 0);



       Rect face_area(real_facesize_x, real_facesize_y, real_facesize_width,real_facesize_height);

       Mat faceROI = img_gray( face_area );

       std::vector<Rect> eyes;


       //-- In each face, detect eyes

       ((CascadeClassifier *) cascadeClassifier_eye)->detectMultiScale( faceROI, eyes, 1.1, 2, 0 |CASCADE_SCALE_IMAGE, Size(30, 30) );


       for ( size_t j = 0; j < eyes.size(); j++ )

       {

           Point eye_center( real_facesize_x + eyes[j].x + eyes[j].width/2, real_facesize_y + eyes[j].y + eyes[j].height/2 );

           int radius = cvRound( (eyes[j].width + eyes[j].height)*0.25 );

           circle( img_result, eye_center, radius, Scalar( 255, 0, 0 ), 30, 8, 0 );

       }

   }



}





2-10. NDK 버전 17.x 대 로 업데이트 후 발생한 에러입니다.


C:\Users\webnautes\AndroidStudioProjects\UseOpenCVwithCMake\app\src\main\cpp/native-lib.cpp:81: undefined reference to `cv::CascadeClassifier::detectMultiScale(cv::_InputArray const&, std::__ndk1::vector<cv::Rect_<int>, std::__ndk1::allocator<cv::Rect_<int> > >&, double, int, int, cv::Size_<int>, cv::Size_<int>)'
C:\Users\webnautes\AndroidStudioProjects\UseOpenCVwithCMake\app\src\main\cpp/native-lib.cpp:103: undefined reference to `cv::CascadeClassifier::detectMultiScale(cv::_InputArray const&, std::__ndk1::vector<cv::Rect_<int>, std::__ndk1::allocator<cv::Rect_<int> > >&, double, int, int, cv::Size_<int>, cv::Size_<int>)'
clang++.exe: error: linker command failed with exit code 1 (use -v to see invocation)
ninja: build stopped: subcommand failed.



build.gradle 파일에 다음 부분(파란색)을 추가하고 Sync Now를 클릭하거나 메뉴에서 File > Sync Project with Gradle Files를 선택하면 해결됩니다.

https://stackoverflow.com/a/50320876


android {
   compileSdkVersion 27
   defaultConfig {
       applicationId "com.tistory.webnautes.useopencvwithcmake"
       minSdkVersion 15
       targetSdkVersion 27
       versionCode 1
       versionName "1.0"
       testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
       externalNativeBuild {
           cmake {
               cppFlags "-frtti -fexceptions"
               arguments '-DANDROID_TOOLCHAIN=gcc',
                       '-DANDROID_STL=gnustl_static'
           
       }
   }
   buildTypes {



이제 안드로이드 디바이스에 설치해보면 됩니다.




3. 캡쳐 버튼 추가하기

영상에 인식된 얼굴 위치, 눈 위치를 그려넣는 코드와 최종 영상을 저장하는 코드간에 동기화를 맞추기 위해서 세마포어를 사용했습니다.


3-1. 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"
   android:orientation="vertical"
   tools:context=".MainActivity" >

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center_horizontal"
       android:text="캡쳐" />

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

</LinearLayout>




3-2. 세마포어를 사용하기 위한 코드를 추가합니다.


   private final Semaphore writeLock = new Semaphore(1);

   public void getWriteLock() throws InterruptedException {
       writeLock.acquire();
   }

   public void releaseWriteLock() {
       writeLock.release();
   }
   

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




3-3. 기존 코드에서 카메라 접근하는 부분을 세마포어로 둘러싸둡니다.


  @Override
   public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {

       try {
           getWriteLock();

           matInput = inputFrame.rgba();

           if ( matResult != null ) matResult.release();
           matResult = new Mat(matInput.rows(), matInput.cols(), matInput.type());

           Core.flip(matInput, matInput, 1);

           detect(cascadeClassifier_face,cascadeClassifier_eye, matInput.getNativeObjAddr(),
                   matResult.getNativeObjAddr());

       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       releaseWriteLock();

       return matResult;
   }




3-4. 버튼 클릭시 사진을 저장하기 위한 코드를 onCreate 메소드에 추가합니다.


       Button button = (Button)findViewById(R.id.button);
       button.setOnClickListener(new View.OnClickListener() {
           public void onClick(View v) {

               try {
                   getWriteLock();

                   File path = new File(Environment.getExternalStorageDirectory() + "/Images/");
                   path.mkdirs();
                   File file = new File(path, "image.png");

                   String filename = file.toString();

                   Imgproc.cvtColor(matResult, matResult, Imgproc.COLOR_BGR2RGB, 4);
                   boolean ret  = Imgcodecs.imwrite( filename, matResult);
                   if ( ret ) Log.d(TAG, "SUCESS");
                   else Log.d(TAG, "FAIL");


                   Intent mediaScanIntent = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                   mediaScanIntent.setData(Uri.fromFile(file));
                   sendBroadcast(mediaScanIntent);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }


               releaseWriteLock();

           }
       });




버튼을 클릭하여 저장한 영상입니다.




참고

https://www.javacodegeeks.com/2012/10/locking-with-semaphore-example.html




4. 인식된 얼굴 갯수 반환받기

native-lib.cpp의 Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_detect 함수 수정


1. 함수 반환형을 jint로 변환합니다.


JNIEXPORT jint JNICALL
Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_detect(JNIEnv *env, jobject instance,




2. 함수에 리턴값 저장할 변수를 선언합니다.


   Mat &img_input = *(Mat *) matAddrInput;
   Mat &img_result = *(Mat *) matAddrResult;

   int ret = 0;


3. 리턴 받을 인식된 얼굴 개수를 ret 변수에 대입합니다.


   //-- Detect faces
   ((CascadeClassifier *) cascadeClassifier_face)->detectMultiScale( img_resize, faces, 1.1, 2, 0|CASCADE_SCALE_IMAGE, Size(30, 30) );


   // 로그값 혼동을 방지하기 위해 주석처리
   //__android_log_print(ANDROID_LOG_DEBUG, (char *) "native-lib :: ",
   //                 (char *) "face %d found ", faces.size());
   ret = faces.size();


4. 변수 ret를 리턴합니다.


return ret;

}



MainActivity.java 수정


1. detect 함수 선언에서 리턴값을 int로 수정합니다.


   public native int detect(long cascadeClassifier_face,
                             long cascadeClassifier_eye, long matAddrInput, long matAddrResult);

 



2. onCameraFrame 메소드에서 detect의 리턴값을 출력합니다.


int ret = detect(cascadeClassifier_face,cascadeClassifier_eye, matInput.getNativeObjAddr(),
matResult.getNativeObjAddr());

if ( ret != 0 )
Log.d(TAG, "face " + ret + " found");

 

 

 

* 내가 고생했던 에러.

경로명이 틀리거나,  프로젝트 이름이 다르다는걸 인지 하지 못하고 복사 붙여 넣기 하여 사용 한점.

 

 

출처 : http://webnautes.tistory.com/1087

728x90
반응형