Android Studio 2.2에서 OpenCV 3.1 세팅하기

OpenCV와 Android Studio 버전에 따라 다른 내용이 생길 수 있으니 주의 바랍니다.

1. 안드로이드 SDK 다운로드

OpenCV 다운로드 페이지에서 OpenCV for Android를 다운 받습니다. 다운 받은 파일을 원하는 위치에 압축 해제합니다. 저는 편리한 접근을 위해 C 드라이브에 바로 압축해제 했습니다.

2. 프로젝트 생성

새롭게 프로젝트를 생성합니다. 안드로이드에서 OpenCV를 사용할 때는 OpenCV JAVA API를 사용할 수도 있고, OpenCV Native API를 JNI 형태로 사용할 수도 있습니다. 후자가 지원하는 기능이 더 많지만 추가로 설정이 필요합니다. 이 문서에서는 JNI를 사용하는 부분까지 전부 다룰 예정이지만 JAVA API 부분만 사용할 예정이라면 C++ 지원은 해제하셔도 됩니다.

다음으로 최소 SDK 버전 설정 페이지가 나오는데, API 21 이상으로 설정합니다. OpenCV 3.1 버전에서는 안드로이드의 camera2 클래스를 사용하기 때문에 최소 SDK 버전을 21 이상으로 맞출 필요가 있습니다. 이후는 기존의 안드로이드 프로젝트 생성과 동일합니다. Empty Activity를 선택했고, C++11 지원을 켠 것 이외에는 모두 기본 설정으로 진행했습니다.

CMake, NDK 등 JNI 사용에 필요한 도구들이 설치되어 있지 않아 에러 메시지가 발생하는 경우 Tools > Android > SDK Manager의 SDK Tools 탭에서 설치할 수 있습니다. 설치 후에 build.gradle 파일을 열어 프로젝트를 동기화 합니다.

3. OpenCV JAVA API 사용하기

File > New > Import Module을 클릭해 모듈을 추가합니다. Source Directory에 <OpenCV Path>/sdk/java를 입력하면 모듈 이름을 자동으로 감지합니다.

다음으로, OpenCVLibrary310/build.gradle 파일을 열어 compileSdkVersion과 buildToolsVersion을 app/build.gradle 파일과 동일하게 맞춥니다. minSdkVersion도 21로 조정합니다. 최종적으로 수정된 제 build.gradle 파일은 다음과 같으며, 사용하는 안드로이드 컴파일 SDK 버전에 따라 수치가 다를 수 있습니다. 수정 후 gradle 파일 sync 버튼을 눌러 빌드가 되는지를 확인합니다.

apply plugin: 'com.android.library'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.2"

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 21
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }
}

다음으로는 모듈 사이의 의존성을 설정해 주어야 합니다. 프로젝트 탐색기에서 프로젝트 폴더를 우클릭하고, Open Module Settings를 클릭한 뒤, app 모듈을 선택하고 Module Dependency로 openCVLibrary310을 추가합니다.

마지막으로, JAVA API 로드가 성공적으로 되었는지를 확인하기 위해 다음 코드를 Main Activity에 추가합니다.

    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                {
                    Log.i("OpenCV", "OpenCV loaded successfully");
                } break;
                default:
                {
                    super.onManagerConnected(status);
                } break;
            }
        }
    };

    @Override
    public void onResume()
    {
        super.onResume();
        OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_1_0, this, mLoaderCallback);
    }

빌드 후 디바이스에서 실행하려고 하면, OpenCV Manager가 설치되어 있지 않다는 메시지와 함께 구글 플레이 스토어로 이동 됩니다. 그대로 설치하면 되고, 에뮬레이터 등 플레이 스토어가 존재하지 않는 환경에서는 <OpenCV Path>/apk에서 CPU 아키텍처에 맞는 apk 파일을 이용해 설치할 수도 있습니다. 이렇게 사용하는 경우 OpenCV를 사용하는 애플리케이션끼리 라이브러리 하나를 서비스 형태로 공유해서 사용할 수 있으며, OpenCV 홈페이지에서 권장하고 있는 방법입니다.

OpenCV Manager 없이 애플리케이션을 standalone 형태로 배포하고 싶은 경우에는 <OpenCV Path>/sdk/native/libs에 있는 라이브러리 파일을 사용해야 하며 여기서는 다루지 않겠습니다. OpenCV 3.1 튜토리얼Static Initialization 부분을 참고하시면 됩니다.

여기까지 완료하셨다면 빌드 후 디바이스에 설치한 뒤, Android Monitor 기능을 이용해 성공적으로 OpenCV API를 호출하는 것을 확인할 수 있습니다.

4. OpenCV JNI로 사용하기

OpenCV의 JAVA API에도 유용한 기능이 많지만, 퍼포먼스가 C++ Native Code보다 떨어지고 지원하는 기능이 적습니다. 이 때 JNI를 이용하면 기존의 C++ OpenCV API를 안드로이드 자바 코드에서 호출해 사용할 수 있습니다.

기존 튜토리얼 대다수가 안드로이드 스튜디오 하위 버전을 기준으로 작성되었고, Deprecated된 기능을 사용하고 있어 JNI 빌드 부분에서 고생을 했습니다. 구글의 안드로이드 스튜디오 가이드가 큰 도움이 되었습니다.

프로젝트를 생성할 때 C++ 지원을 활성화 하셨다면 CMakeLists.txt와 함께 native-lib.cpp 파일이 생성되셨을 겁니다. CMakeLists.txt에 다음 내용을 추가합니다. OpenCV Path는 자신의 설치 경로로 바꿔주시기 바랍니다.

set(OpenCV_DIR <OpenCV Path>/sdk/native/jni)
find_package(OpenCV REQUIRED)

그리고 target_link_libraries의 마지막에 ${OpenCV_LIBS}를 추가합니다. IDE 동기화가 성공적으로 이루어지는지 확인합니다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="io.qwaz.androidopencv.MainActivity">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:srcCompat="@android:color/transparent"
        android:layout_below="@+id/sample_text"
        android:id="@+id/imageView"
        android:layout_alignParentStart="true"
        android:adjustViewBounds="false"
        android:cropToPadding="false"
        android:layout_marginTop="16dp" />
</RelativeLayout>
#include <jni.h>
#include <string>
#include <opencv2/opencv.hpp>

extern "C" {

JNIEXPORT jstring JNICALL
Java_io_qwaz_androidopencv_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

JNIEXPORT void JNICALL
Java_io_qwaz_androidopencv_MainActivity_fillMatrixColor(
        JNIEnv* env,
        jobject,
        jlong addrMat,
        jint red,
        jint green,
        jint blue
) {
    using namespace cv;

    Mat &mat = *(Mat*)addrMat;
    mat = Scalar(red, green, blue);

    return;
}

}
package io.qwaz.androidopencv;

import android.graphics.Bitmap;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;

import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                {
                    Log.i("OpenCV", "OpenCV loaded successfully");
                    // OpenCV 초기화 이후 함수 호출
                    updateImageView();
                } break;
                default:
                {
                    super.onManagerConnected(status);
                } break;
            }
        }
    };

    @Override
    public void onResume()
    {
        super.onResume();
        OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_1_0, this, mLoaderCallback);
    }

    private void updateImageView() {
        ImageView iv = (ImageView) findViewById(R.id.imageView);
        iv.getImageMatrix();

        Mat mat = Mat.zeros(iv.getHeight(), iv.getWidth(), CvType.CV_8UC3);
        fillMatrixColor(mat.getNativeObjAddr(), 200, 1, 80);

        Bitmap bmp = Bitmap.createBitmap(mat.cols(), mat.rows(), Bitmap.Config.ARGB_8888);
        Utils.matToBitmap(mat, bmp);

        iv.setImageBitmap(bmp);
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
    private native void fillMatrixColor(long addrMat, int red, int green, int blue);

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }
}

screenshot_2016-10-16-16-48-29

위와 같이 ImageView에 색깔이 입혀진 것을 확인할 수 있습니다.

9 댓글

  1. 먼저, 군더더기없는 글이라서 따라하는 데 별 어려움없었습니다. 내용이 알차서 여러날 헛된 구글링을 한방에 만회하는 글입니다. 고맙습니다.

    set(OpenCV_DIR /sdk/native/jni) 부분에서 조금 헤맸었는데요, 넣을 때
    c:\이 아니라 c:/을 해야 한다는 것! (***초보자이신 분들만 참고~)

    콰즈님, OpenCV JNI로 나가려는데요, native_lib.cpp에 쓸 수 있는 openCV 예제들은 어디가면 찾을 수 있나요?

    1. JNI를 쓰시는 경우 기존에 쉽게 찾으실 수 있는 OpenCV C++ 예제 코드를 거의 수정하지 않고 그대로 사용할 수 있습니다. 제 경험으로는 GUI나 GPU 관련 API 빼면 나머지는 큰 수정 없이 동작했습니다.

      jlong / getNativeObjectAddr 같이 JNI 콜링 컨벤션 부분만 조금 보시면 될 거라 생각해요.

  2. 글 잘봤습니다.
    java와 OpenCV에 막 입문하는 초보입니다. 콰즈님의 글대로 진행을 하는 중에
    4번에서 컴파일을 하고 Run하는 과정에서 예외가 발생하면서 어플리케이션이 종료되는 문제가 있어 질문드리려고 합니다.

    E/AndroidRuntime: FATAL EXCEPTION: main
    Process: aseall.opencv_qwaz, PID: 27865
    java.lang.IllegalArgumentException: width and height must be > 0
    at android.graphics.Bitmap.createBitmap(Bitmap.java:933)
    at android.graphics.Bitmap.createBitmap(Bitmap.java:912)
    at android.graphics.Bitmap.createBitmap(Bitmap.java:879)
    at aseall.opencv_qwaz.MainActivity.updateImageView(MainActivity.java:61)
    at aseall.opencv_qwaz.MainActivity.access$000(MainActivity.java:17)
    at aseall.opencv_qwaz.MainActivity$1.onManagerConnected(MainActivity.java:37)
    at org.opencv.android.AsyncServiceHelper$3.onServiceConnected(AsyncServiceHelper.java:319)
    at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:1265)
    at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:1282)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:145)
    at android.app.ActivityThread.main(ActivityThread.java:5942)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194)

    위와같은 예외가 발생했는데 Bitmap 라이브러리의 createBitmap메소드를 확인해보니

    private static Bitmap createBitmap(int width, int height, Config config, boolean hasAlpha) {
    return createBitmap(null, width, height, config, hasAlpha);
    }
    가 아닌

    public static Bitmap createBitmap(int colors[], int offset, int stride,
    int width, int height, Config config) {
    return createBitmap(null, colors, offset, stride, width, height, config);
    }
    를 호출하면서 에러가 발생했습니다.

    동시에 Bitmap 라이브러리에서 import하고 있던 android.annotation.의 CheckResult, ColorInt, NonNull과 libcore.util.NativeAllocationRegistry를 찾을 수 없다고 나옵니다.

    제가 파악한게 예외 발생 원인이 맞는건지, 해결하기 위해서 어떤걸 더 찾아봐야하는지 모르겠습니다.

    답변 주시면 감사하겠습니다.

  3. 안녕하세요 opencv입문하고있는데 잘 참고하고있습니다. 그런데
    프로젝트를 생성할 때 C++ 지원을 활성화 하셨다면 CMakeLists.txt와 함께 native-lib.cpp 파일이 생성되셨을 겁니다. << 이부분에서 CMakeLists.txt와 함께 native-lib.cpp 파일이 c++서포트를 체크함에도 불구하고 찾을수가없는데 어떤경로를 통해 찾아야하나요 ?

    1. JNI 설정하기 안되네여 ;;ㅋㅋ
      set(/sdk/native/jni)
      find_package(OpenCV REQUIRED)
      이렇게했는데 자꾸 빌드하면 오류납니다.ㅋ

  4. 저도 이 글을 따라하면서 방금 같은 현상을 겪었습니다.

    저는 MainActivity.java의 58 Line을

    Mat mat = Mat.zeros(iv.getHeight()+1, iv.getWidth()+1, CvType.CV_8UC3);

    요렇게 고쳐서 해결했습니다.
    행렬 크기에 각각 1씩 더해서 0이 안되게 만드는 꼼수를 쓴거죠 -_-;;;;
    정확한 원인과 정식 해결책은 다른 고수님들께서 설명해주시면 감사하겠습니다.

    1. 위 댓글은 Aseall 님 질문에 대한 답입니다;;;; (대댓글이 달릴 거라고 착각했네요;;;;)
      그리고 콰즈님 좋은 글 덕분에 안드로이드 완전 초보인 저도 opencv 까지 적용해볼 수 있었습니다. 감사합니다.

  5. 덕분에 opencv를 무사히 적용했습니다. 정말 감사합니다. 사실 블로그 글들이 너무 좋아서 다른 글들도 읽어봤는데 대학교 때 처음으로 프로그래밍을 접하면서 멍청한 제자신이 느껴지는 좋은 동기부여가 되는 프로젝트들이 많아서 큰 깨달음을 동시에 얻고 갑니다. 감사합니다ㅎㅎ

댓글 남기기