반응형

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. 검출된 얼굴 갯수 반환받기



26으로 낮추어서 다시 해보기





2016. 12. 09 최초작성

2019. 08. 15  OpenCV 4.1.1용 추가

2020. 06. 27 Android 10.0, OpenCV 4.3

2021. 05. 05 Andorid 11.0, OpenCV 4.5.1

2022. 06. 01 Android 12.0, OpenCV 4.5.5, 퍼미션 관련 코드 수정

                     ndk-build 방식에서 문제가 많이 발생하여 제외시켰습니다.

2023. 08. 12 Android 12.0, OpenCV 4.8

        퍼미션 문제로 Android 12.0으로 낮추어서 진행했습니다.

 

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

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

 

 

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




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

 

 

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




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

 

 

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

이상하게도 화면이 세로( Portrait ) 방향일 때에는 검출이 안되고  아래 화면처럼 가로(Landscape) 방향일 때에만 검출이 됩니다.

Portrait 방향일때 안되는 것은 OpenCV Camera API 문제인 듯 싶습니다. 

 

 

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

가장 큰 원인은 장소마다 조명 상태가 다르기 때문입니다.  적절한 조명 상태에서 잘 검출이 됩니다.  






2. 코드 수정 및 사용방법

 

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

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

 

Android 13 + NDK 25.2 + OpenCV 4.8 카메라 예제 프로젝트 생성방법

https://webnautes.blog/4911/ 




퍼미션 문제로 Android SDK 버전을 33에서 32로 낮춰야 합니다. 다음 파일을 수정합니다.



compileSdk와 targetSdk를 33에서 32로 변경합니다.



androidx.appcompat:appcompat의 버전을 1.6.1에서 1.5.1로 변경하고 Sync Now를 클릭합니다.



앱을 실행시켜 봤을때 카메라 영상이 보여야합니다.




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

 



assets라고 입력하고 엔터를 누르면  assets 이름의 디렉토리가 생성됩니다. 

 

 



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

Twibap 님이 알려주신 방법입니다. (크롬 웹브라우저에서만 가능한 방법입니다. )

 

저장된 파일 이름을 haarcascade_eye_tree_eyeglasses.xml와 haarcascade_frontalface_alt.xml로 각각 변경합니다.

 

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



Ctrl키를 누른채 파일을 드래그하여 프로젝트 패널의 assets 디렉토리 위에서 왼쪽 마우스 버튼을 뗀 후, Refactor 버튼을 클릭하면 복사가 됩니다.

 



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

 

 

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

 




API 29이상에서 필요한 부분입니다.

 

   android:requestLegacyExternalStorage="true"

 





2-5.  android 라이브러리를 사용하도록  CMakeLists.txt 파일을 수정합니다.  수정후 Sync Now를 클릭해줘야 합니다. 

 

 

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} )




오른쪽 상단에 보이는 Sync Now를 클릭합니다. 

또는 메뉴에서 File > Sync Project with Gradle Files를 선택합니다.





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

 




수정 1.  다음 파란색 코드들을 추가 또는 수정합니다.

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



    protected void onCameraPermissionGranted() {
        List<? extends CameraBridgeViewBase> cameraViews = getCameraViewList();
        if (cameraViews == null) {
            return;
        }
        for (CameraBridgeViewBase cameraBridgeViewBase: cameraViews) {
            if (cameraBridgeViewBase != null) {
                cameraBridgeViewBase.setCameraPermissionGranted();

                read_cascade_file();
            }
        }
    }




    @Override
    protected void onStart() {
        super.onStart();
        boolean havePermission = true;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(CAMERA) != PackageManager.PERMISSION_GRANTED
                    || checkSelfPermission(WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
            ) {
                requestPermissions(new String[]{CAMERA, WRITE_EXTERNAL_STORAGE}, CAMERA_PERMISSION_REQUEST_CODE);
                havePermission = false;
            }
        }
        if (havePermission) {
            onCameraPermissionGranted();
        }
    }

 

  @Override
    @TargetApi(Build.VERSION_CODES.M)
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == CAMERA_PERMISSION_REQUEST_CODE && grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED
                && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
            onCameraPermissionGranted();
        }else{
            showDialogForPermission("앱을 실행하려면 퍼미션을 허가하셔야합니다.");
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }


    @TargetApi(Build.VERSION_CODES.M)
    private void showDialogForPermission(String msg) {

        AlertDialog.Builder builder = new AlertDialog.Builder( MainActivity.this);
        builder.setTitle("알림");
        builder.setMessage(msg);
        builder.setCancelable(false);
        builder.setPositiveButton("예", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id){
                requestPermissions(new String[]{CAMERA, WRITE_EXTERNAL_STORAGE}, CAMERA_PERMISSION_REQUEST_CODE);
            }
        });
        builder.setNegativeButton("아니오", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface arg0, int arg1) {
                finish();
            }
        });
        builder.create().show();
    }




수정 2.  사용하는 카메라를 변경합니다. 0을 1로 변경합니다. 에뮬레이터로 하는 경우에는 웹캠을 에뮬레이터에 연결한 후,  0으로 두어야 합니다. 

 

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        setContentView(R.layout.activity_main);

        mOpenCvCameraView = (CameraBridgeViewBase)findViewById(R.id.activity_surface_view);
        mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);
        mOpenCvCameraView.setCvCameraViewListener(this);
        mOpenCvCameraView.setCameraIndex(1); // front-camera(1),  back-camera(0)
    }



수정 3.  기존 코드를 주석처리하고 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;




수정 4.  xml 파일을 가져오기 위한 메소드를 추가합니다. 

 

  • cpp 파일의 loadCascade 함수를 호출하도록 구현되어있는데 자바 함수를 사용하도록 변경해도 됩니다.
  • 현재 이미 파일을 copyFile 메소드를 이용해서 가져온 경우에 대한 처리가 빠져있습니다.  

 

 

private void copyFile(String filename) {

        AssetManager assetManager = this.getAssets();
        File outputFile = new File( getFilesDir() + "/" + filename );

        InputStream inputStream = null;
        OutputStream outputStream = null;

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

            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( getFilesDir().getAbsolutePath() + "/haarcascade_frontalface_alt.xml");
        Log.d(TAG, "read_cascade_file:");

        cascadeClassifier_eye = loadCascade(  getFilesDir().getAbsolutePath() +"/haarcascade_eye_tree_eyeglasses.xml");
    }



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

 




수정 5. 기존 코드를 주석처리하고 얼굴 검출하는 cpp 코드를 호출하는 코드를 추가합니다. 

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

 

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



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

        matInput = inputFrame.rgba();

        if ( matResult == null )

            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.  JNI 함수를 위한 처리입니다.  

 



자바 코드에서 loadCascade 메소드에 각각  마우스 커서를 가져가면 보이는 빨간 전구를 클릭한 후, 

 

 

보이는 메뉴에서  Create JNI function을 클릭합니다.

 

 

native-lib.cpp를 선택합니다. 

 

 

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



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

 



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

 



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





 

#include <jni.h>
#include <opencv2/opencv.hpp>

#include <android/log.h>

using namespace std;
using namespace cv;

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 thiz, jstring cascade_file_name) {



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

    string baseDir("");
    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(cascade_file_name, nativeFileNameString);

    return ret;


}


extern "C"
JNIEXPORT void JNICALL
Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_detect(JNIEnv *env, jobject thiz,
                                                                  jlong cascade_classifier_face,
                                                                  jlong cascade_classifier_eye,
                                                                  jlong mat_addr_input,
                                                                  jlong mat_addr_result) {


   Mat &img_input = *(Mat *) mat_addr_input;
    Mat &img_result = *(Mat *) mat_addr_result;

    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 *) cascade_classifier_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 *) cascade_classifier_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 );
        }
    }


}






테스트에 사용할 Android 에뮬레이터의 디바이스를 API 32로 사용했습니다. API 33에선 수정후, 카메라가 동작하지 않습니다.



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

얼굴이 인식되는 것을 볼 수 있습니다. 안드로이드폰을 가로로 돌려야 인식이됩니다.




반응형

문제 발생시 지나치지 마시고 댓글 남겨주시면 가능한 빨리 답장드립니다.

도움이 되셨다면 토스아이디로 후원해주세요.
https://toss.me/momo2024


제가 쓴 책도 한번 검토해보세요 ^^

+ Recent posts