반응형

NDK를 지원하는 안드로이드 프로젝트를 생성한 후, OpenCV 라이브러리를 추가하여 사용하는 방법을 설명합니다. 

 

간단한 예제로 OpenCV Java API에서 캡처한 영상을 OpenCV C++ API에서 그레이스케일 영상으로 변환하고 다시  OpenCV Java API에서 화면에 보여줍니다. 



아래 리스트 중 2022. 5. 7에 명시된 버전으로만 테스트를 진행했으며 다른 버전 사용시 문제가 발생할 가능성이 있습니다. 




2016.11.20 : 최초 작성 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

2019. 8. 8 

OpenCV 4.1.1를 위해 카메라 코드를 수정하고 android.support 대신에 androidx를 사용합니다. 

2020. 1. 19  OpenCV 4.2.0

2020. 6. 20 OpenCV 4.3.0, Android Studio 4.0, Android 10, NDK 21

2021. 1. 6  OpenCV 4.5.1, Android Studio 4.1.1, Android 11, NDK 21

2021. 4. 10  OpenCV 4.5.2, Android Studio 4.1.3, Android 11, NDK 21

2021. 9. 1 Android Studio 2020.3.1에서 OpenCV 모듈을 가져오려고 하면 위자드에서 Finish 버튼이 비활성화 상태로 유지되는 버그가 생긴듯합니다. 해결되기 전까진 수동으로 sdk 폴더를 안드로이드 프로젝트로 옮기고 build.gradle 파일을 수정해서 사용해야 하는 듯합니다.  자세한 내용은 https://stackoverflow.com/a/68738767 

2021. 10. 22 질문이 종종있어서 앞에서 언급했던 링크대로 포스트를 진행해보았습니다.
                      Android Studio Arctic Fox 2020.3.1 Patch 3

                      Android 11.0 API 30 

                      OpenCV 4.5.4

                      NDK 23.1 실제로는 하위 버전을 사용할 수도 있습니다.

2022. 5. 7  아래사항으로 업데이트 후, 안드로이드 에뮬레이터로 테스트

                  Android Studio Bumblebee 2021.1.1 Patch 3

                  Android 12.0 API 31, Minimum SDK API 21로 설정

                  OpenCV 4.5.5

                  NDK 33-rc3




            0. 패키지 설치

1. 안드로이드 프로젝트 생성

2. 프로젝트에 OpenCV 라이브러리 추가

3. CMake 사용한 NDK + OpenCV 카메라 예제

4. 참고


관련 포스트

 

OpenCV와 NDK를 사용하여 Android에서 Face Detection(얼굴 검출)

https://webnautes.tistory.com/1087

 

0. 패키지 설치

 

1. 스크린샷처럼 SDK Manager를 실행합니다. 또는 안드로이드 스튜디오 메뉴에서 Tools > SDK Manager를 선택합니다.

 



2. 안드로이드 스튜디오에서 CMake를 사용하여 C/C++ 코드를 사용하기 위해서는 다음 패키지가 필요합니다.

 

  • The Android Native Development Kit (NDK)

안드로이드에서 JAVA 코드와 C/C++ 코드를 같이 사용할 수 있게 해줍니다.

 

  • CMake



SDK Tools 탭에서  NDK, CMake를 선택하고 Apply 버튼을 클릭하면 다운로드 및 설치가 진행됩니다.

포스트 진행 과정에서 추가로 다른 버전의 NDK가 설치될 수 있습니다. 

 



Android 12 API 31을 설치한 상태에서 진행했습니다.

 



3. 에뮬레이터를 생성해서 사용했습니다. Virtual Device Manager를 실행합니다. 

 



Create device를 클릭합니다.

 



Device로 Pixel XL을 선택하고 Next를 클릭합니다. 

 



S API 31에 보이는 Download를 클릭하여 시스템 이미지를 다운로드한 후, S  API 31가 선택된 상태에서 Next를 클릭합니다. 

 



왼쪽 아래에 보이는 Show Advanced Settings를 클릭합니다.

 



Camera 항목에 있는 Front와 Back을 모두 Webcam0로 바꾸로 Finish를 클릭합니다.

 



이제 에뮬레이터가 생성되었습니다. 

 



1. 안드로이드 프로젝트 생성

 

1-0.  New Project를 선택합니다. 

 



1-1. Native C++를 선택후, Next를 클릭합니다. 

 



1-2. Name 항목에 프로젝트 이름으로 “Use OpenCV with CMake”을 적고, Language는 Java,  Minimum API level은 API 21을 선택한 후, Next 버튼을 클릭합니다. 

 

본 포스트는 프로젝트 이름을 “Use OpenCV with CMake”로 한경우에 대해 작성되었기 때문에 프로젝트 이름이 다른 경우에는 변경이 필요한 부분이 있습니다. 포스트에 명시해놓았습니다.

 



1-3. 디폴트 값으로 두고 Finish 버튼을 클릭하면 됩니다.

 



2. 프로젝트에 OpenCV 라이브러리 추가

2-1. OpenCV 깃허브 저장소에서 opencv-4.5.5-android-sdk.zip 파일을 다운로드 합니다. 

https://github.com/opencv/opencv/releases 






압축을 풀어서 C:\에 복사해줍니다. 




2-2.  Android용 OpenCV가 다음 위치에 있는 것으로 가정하고 진행합니다.

C:\OpenCV-android-sdk

 




2-3.  현재는 이 방법은 진행하는데 문제가 있습니다. 문제가 수정될 때까지는  번거롭더라도 수동으로 모듈을 가져오는  2-4를 진행하세요. 참고 https://stackoverflow.com/a/68738767 

 

다음 버전에서 확인결과 해결되었습니다. 

Android Studio Bumblebee 2021.1.1 Patch 3



앞에서 진행한 프로젝트 생성이 완료되기를 대기합니다. 안드로이드 스튜디오의 상태 표시줄에 작업중 메시지가 사라져야 합니다. 

 



OpenCV 라이브러리 모듈을 프로젝트로 가져오기 위해 메뉴에서 File > New > Import Module를 선택합니다. 

 

Source directory 입력란 옆에 있는 버튼을 클릭합니다.

 



OpenCV-android-sdk 디렉토리 하위에 있는 sdk 디렉토리를 선택하고 OK 버튼을 클릭합니다. 

 




Finish 버튼을 클릭합니다.

 



2-4. 다음과 같은 에러가 발생합니다.

 

A problem occurred evaluating project ':sdk'.

> Plugin with id 'kotlin-android' not found.



현재 프로젝트 창에 sdk 모듈이 보이지 않습니다. 왼쪽 위에 있는 Android를 클릭하여 보이는 목록에서 Project를 클릭하여 프로젝트 창에 보여지는 것 Project 뷰로 변경해야 sdk 모듈이 보이게 됩니다. 

 



sdk 모듈(opencv)이 추가되었다면 다음처럼 보입니다. 

 



sdk 에 있는 build.gradle를 클릭합니다. 

 



상단에 보이는 apply plugin: 'kotlin-android'를 제거합니다.

 

변경전

 



변경후

 




오른쪽 위에 보이는 Try Again을 클릭합니다. 

 



문제 없으면 다음 로그가 보입니다.

 



build.gradle (:sdk)에서 targetSdkVersion 26에 빨간줄이 보이는데 Google Play를 사용하지 않으면 상관없어 보입니다. 실행시 문제가 되지 않았습니다. 

 



2-5. app 모듈에서 opencv 라이브러리 모듈을 사용하도록 설정해줘야 합니다.

메뉴에서 File > Project structure를 선택한 후,  왼쪽에 보이는 리스트에서 Dependencies를 선택합니다. 

 




Modules에서  app을 선택한 후, Declared Dependencies 문자열 아래에 보이는 +를 클릭하면 보이는 메뉴에서 Module Dependency를 선택합니다. 

 



앞에서 추가했던 opencv 모듈이 sdk 이름으로 보입니다. 체크한 후  OK버튼을 클릭합니다.

 



이제 sdk 모듈(opencv) app 모듈에서 사용할 수 있게 설정되었습니다. 

이제 OK 버튼을 클릭하여 Project Structure 창을 닫습니다.

 



2-6.  문제없다면 다음 로그가 보입니다.

 

 

 

3. CMake 사용한 NDK + OpenCV 카메라 예제

 

3-1. 이제 프로젝트 창을 Android 뷰로 변경합니다.

 



3-2. AppCompatActivity 클래스를 사용한 액티비티에서 타이틀바를 없애기 위해서 themes.xml 파일에 다음 코드를 추가합니다.

 





    <!-- Base application theme. -->
    <style name="Theme.UseOpenCVWithCMake" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
        <!-- No Title Bar-->
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>
</resources>



3-3. 레이아웃 파일 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=".MainActivity" >
   
    <org.opencv.android.JavaCameraView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/activity_surface_view" />

</LinearLayout>



3-4. 매니페스트 파일 AndroidManifest.xml 에  다음 코드를 추가합니다. 

 

 

<?xml version="1.0" encoding="utf-8"?>

    package="com.tistory.webnautes.useopencvwithcmake">

    <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"/>

    <supports-screens android:resizeable="true"
        android:smallScreens="true"
        android:normalScreens="true"
        android:largeScreens="true"
        android:anyDensity="true" />
   
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.UseOpenCVWithCMake">
        <activity
            android:name=".MainActivity"
            android:screenOrientation="landscape"
            android:configChanges="keyboardHidden|orientation"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

 

3-5. 카메라로부터 영상을 가져오는 것은 JAVA 코드에서 하며 JNI(Java Native Interface)를 사용하여 C/C++ 함수를 호출하여 영상처리를 진행합니다. 

 

자바코드 파일 MainActivity.java를 다음 코드로 대체합니다.

기존 코드에서 첫번째 줄에 있는 package는 남겨둬야 합니다. 

 

 

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.content.DialogInterface;
import android.os.Bundle;
import android.annotation.TargetApi;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import android.view.SurfaceView;
import android.view.WindowManager;
import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.Mat;

import java.util.Collections;
import java.util.List;

import static android.Manifest.permission.CAMERA;


public class MainActivity extends AppCompatActivity
        implements CameraBridgeViewBase.CvCameraViewListener2 {

    private static final String TAG = "opencv";
    private Mat matInput;
    private Mat matResult;

    private CameraBridgeViewBase mOpenCvCameraView;

    public native void ConvertRGBtoGray(long matAddrInput, long matAddrResult);


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



    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                {
                    mOpenCvCameraView.enableView();
                } break;
                default:
                {
                    super.onManagerConnected(status);
                } break;
            }
        }
    };


    @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(0); // front-camera(1),  back-camera(0)
    }

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

    @Override
    public void onResume()
    {
        super.onResume();

        if (!OpenCVLoader.initDebug()) {
            Log.d(TAG, "onResume :: Internal OpenCV library not found.");
            OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_2_0, this, mLoaderCallback);
        } else {
            Log.d(TAG, "onResum :: OpenCV library found inside package. Using it!");
            mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
        }
    }


    public void onDestroy() {
        super.onDestroy();

        if (mOpenCvCameraView != null)
            mOpenCvCameraView.disableView();
    }

    @Override
    public void onCameraViewStarted(int width, int height) {

    }

    @Override
    public void onCameraViewStopped() {

    }

    @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());

        return matResult;
    }


    protected List<? extends CameraBridgeViewBase> getCameraViewList() {
        return Collections.singletonList(mOpenCvCameraView);
    }


    //여기서부턴 퍼미션 관련 메소드
    private static final int CAMERA_PERMISSION_REQUEST_CODE = 200;


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

    @Override
    protected void onStart() {
        super.onStart();
        boolean havePermission = true;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(CAMERA) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{CAMERA}, 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) {
            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}, CAMERA_PERMISSION_REQUEST_CODE);
            }
        });
        builder.setNegativeButton("아니오", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface arg0, int arg1) {
                finish();
            }
        });
        builder.create().show();
    }


}

 

3-6. MainActivity.java 파일에 다음처럼 ConvertRGBtoGray 함수가 빨간색으로 보입니다. 

 



3-7. 마우스 커서를 ConvertRGBtoGray 함수에 가져가면 보이는 빨간전구를 클릭하고 메뉴에서 “Create JNI function for ConvertRGBtoGray”를 선택합니다.

 



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

 



다음 코드를 참고하여 빨간색 부분을 제거하고 파란색 부분을 추가합니다.

 

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

using namespace cv;


extern "C" JNIEXPORT jstring JNICALL
Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_ConvertRGBtoGray(JNIEnv *env,
                                jobject thiz,
                                jlong mat_addr_input,
                                jlong mat_addr_result) {
    // TODO: implement ConvertRGBtoGray()
    Mat &matInput = *(Mat *)mat_addr_input;
    Mat &matResult = *(Mat *)mat_addr_result;

    cvtColor(matInput, matResult, COLOR_RGBA2GRAY);
}

 

빨간색으로 보이는 건 무시하세요. 다음 단계를 진행하면 해결됩니다.



3-8.  CMakeLists.txt를 다음 코드로 대체합니다. 

 

 

pathPROJECT는 실제 안드로이드 프로젝트 경로로 변경해야 합니다. 

 

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

set(pathPROJECT C:/Users/webnautes/AndroidStudioProjects/UseOpenCVwithCMake) # 수정필요
set(pathOPENCV ${pathPROJECT}/sdk)
set(pathLIBOPENCV_JAVA ${pathOPENCV}/native/libs/${ANDROID_ABI}/libopencv_java4.so)

set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")

include_directories(${pathOPENCV}/native/jni/include)


# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

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).
        ${pathPROJECT}/app/src/main/cpp/native-lib.cpp )



add_library( lib_opencv SHARED IMPORTED )

set_target_properties(lib_opencv PROPERTIES IMPORTED_LOCATION ${pathLIBOPENCV_JAVA})


# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

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 )

# 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

        lib_opencv

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



3-9. Sync Now를 클릭하거나 메뉴에서 File > Sync Project with Gradle Files를 선택하여 Gradle build를 시작합니다.



문제 없으면 다음과 같은 로그가 보입니다.

 



안드로이드 폰에 설치하여 실행시켜 보면  안드로이드폰의 방향에 따라  카메라 영상도 같이 회전합니다.  



포스트에서는 에뮬레이터를 사용하여 진행했습니다. 

스크린샷은 안드로이드 버전따라 차이가 있을 수 있습니다. 



앞에서 생성한 에뮬레이터가 선택된 상태에서 실행합니다.

 



While using the app을 선택합니다.

 



카메라 영상이 에뮬레이터 화면에 보여집니다. 

 



가로로 고정되도록 되어있기 때문에 안드로이드 폰을 가로로 두고 봐야 합니다.

전체화면으로 카메라 영상을 보기 위해선 가로로 영상을 봐야 했기 때문입니다. 




참고

 

https://developer.android.com/ndk/guides/index.html

 

https://developer.android.com/studio/projects/add-native-code.html

 

https://github.com/googlesamples/android-ndk

 

http://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html

 

http://docs.opencv.org/2.4/doc/tutorials/introduction/android_binary_package/dev_with_OCV_on_Android.html

 

반응형

진행해본 결과물을 기록 및 공유하는 공간입니다.
잘못된 부분이나 개선점을 알려주시면 반영하겠습니다.


소스코드 복사시 하단에 있는 앵커 광고의 왼쪽 위를 클릭하여 닫은 후 해야 합니다.


문제가 생기면 포스트와 바뀐 환경이 있나 먼저 확인해보세요.
질문을 남겨주면 가능한 빨리 답변드립니다.


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

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기

댓글을 달아 주세요

TistoryWhaleSkin3.4">
  1. 이전 댓글 더보기