https://webnautes.tistory.com/923?category=704164
다음 순서로 설명합니다.
1. 안드로이드 프로젝트 생성
2. 프로젝트에 OpenCV 라이브러리 추가
3. ndk-build 사용한 NDK + OpenCV 카메라 예제
4. 참고
|
최종 수정 - 2019. 6. 7 : Android Studio 3.4.1로 테스트
업데이트 날짜 기준으로 최신 버전으로 안드로이드 스튜디오 및 패키지를 업데이트 후 확인했습니다.
1. 안드로이드 프로젝트 생성
1-1. Empty Activity를 선택합니다.
1-2. Name 항목에 프로젝트 이름을 적고, Minimum API level은 API 21을 선택합니다.
2. 프로젝트에 OpenCV 라이브러리 추가
2-1. OpenCV를 위한 깃허브에서 opencv-4.1.0-android-sdk.zip 파일을 다운로드 합니다.
https://github.com/opencv/opencv/releases
압축을 풀어서 C:\에 복사해줍니다.
2-2. Android용 OpenCV가 다음 위치에 있는 것으로 가정하고 진행합니다.
C:\OpenCV-android-sdk
2-3. 앞에서 진행한 프로젝트 생성이 완료되기를 대기합니다.
OpenCV 라이브러리 모듈을 프로젝트로 가져오기 위해 메뉴에서 File > New > Import Module를 선택합니다.
Source directory 입력란 옆에 있는 버튼을 클릭합니다.
OpenCV-android-sdk 디렉토리 하위에 있는 sdk 디렉토리를 선택하고 OK 버튼을 클릭합니다.
Module name을 opencv로 수정하고 Finish 버튼을 클릭합니다.
다음과 같은 에러가 나는 경우
ERROR: The minSdk version should not be declared in the android manifest file. You can move the version from the manifest to the defaultConfig in the build.gradle file.
Remove minSdkVersion and sync project
Affected Modules: opencv
프로젝트 창에서 opencv 모듈의 메니페스트를 선택하고
메니페스트에서 다음 부분을 삭제하고 Try Again를 클릭합니다.
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="21" />
2-4. app 모듈에서 opencv 라이브러리 모듈을 사용하도록 설정해줘야 합니다.
메뉴에서 File > Project structure를 선택한 후, 왼쪽에 보이는 리스트에서 Dependencies를 선택합니다.
Modules에서 app을 선택한 후, Declared Dependencies에 보이는 +를 클릭하면 보이는 메뉴에서 Module Dependency를 선택합니다.
앞에서 추가했던 opencv 모듈이 보입니다. 체크한 후 OK버튼을 클릭합니다.
이제 opencv 모듈을 app 모듈에서 사용할 수 있게 설정되었습니다.
이제 OK 버튼을 클릭하여 Project Structure 창을 닫습니다.
2-5. 문제없이 opencv 모듈이 추가되었다면 다음처럼 보입니다.
..
..
3. ndk-build 사용한 NDK + OpenCV 카메라 예제
3-1. 안드로이드 스튜디오에서 ndk-build를 사용하여 C/C++ 코드를 컴파일 및 디버그하기 위해서는 다음 2가지가 필요합니다.
안드로이드에서 JAVA 코드와 C/C++ 코드를 같이 사용할 수 있게 해줍니다.
C/C++ 코드를 디버그하기 위해 사용되는 디버거입니다. 설치해주면 예전에 잡히지 않았던 에러나
예외상황이 검출되며 에러 발생한 C/C++ 코드 위치를 알려줍니다.
안드로이드 스튜디오 메뉴에서 Tools > SDK Manager를 선택합니다.
메뉴에 SDK Manager 항목이 보이지 않는다면 툴바에서 아래 아이콘을 클릭합니다.
SDK Tools 탭에서 NDK, LLDB를 선택하고 Apply 버튼을 클릭하면 다운로드 및 설치가 진행됩니다.
3-2. AppCompatActivity 클래스를 사용한 액티비티에서 타이틀바를 없애기 위해서 styles.xml 파일에 다음 코드(노란색)를 추가합니다.
<resources>
<!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> <!-- No Title Bar--> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> </style>
</resources> |
그리고 상태바도 없애기 위해 MainActivity의 onCreate() 메소드에 다음 코드(노란색)가 필요합니다.
나중에 언급하는 자바 코드에 포함되어 있으므로 여기에선 추가할 필요 없습니다.
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); |
참고 http://commin.tistory.com/63
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"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tistory.webnautes.useopencvwithndk_build">
<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/AppTheme"> <activity android:name=".MainActivity" android:screenOrientation="landscape" android:configChanges="keyboardHidden|orientation">
<intent-filter> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>
</manifest>
|
앱에서 안드로이드 디바이스의 카메라에 접근하기 위해서는 필요한 퍼미션 입니다.
<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"/> |
android:screenOrientation 속성을 landscape로 해주어야 OpenCV JAVA API를 사용하여 전체화면에 카메라 영상을 보여 줄 수 있습니다.
android:screenOrientation="landscape" |
3-5. JNI(Java Native Interface)를 사용하여 C/C++ 함수를 호출하는 JAVA 코드를 작성합니다.
자바코드 파일 MainActivity.java를 다음 코드로 대체합니다.
package com.tistory.webnautes.useopencvwithndk_build;
import android.annotation.TargetApi; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; 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;
public class MainActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener2 {
private static final String TAG = "opencv"; private CameraBridgeViewBase mOpenCvCameraView; private Mat matInput; private Mat matResult;
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);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //퍼미션 상태 확인 if (!hasPermissions(PERMISSIONS)) {
//퍼미션 허가 안되어있다면 사용자에게 요청 requestPermissions(PERMISSIONS, PERMISSIONS_REQUEST_CODE); } }
mOpenCvCameraView = (CameraBridgeViewBase)findViewById(R.id.activity_surface_view); mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE); mOpenCvCameraView.setCvCameraViewListener(this); mOpenCvCameraView.setCameraIndex(0); // front-camera(1), back-camera(0) mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS); }
@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.release(); fix 2018. 8. 18 if ( matResult == null ) matResult = new Mat(matInput.rows(), matInput.cols(), matInput.type());
ConvertRGBtoGray(matInput.getNativeObjAddr(), matResult.getNativeObjAddr());
return matResult; }
//여기서부턴 퍼미션 관련 메소드 static final int PERMISSIONS_REQUEST_CODE = 1000; String[] PERMISSIONS = {"android.permission.CAMERA"};
private boolean hasPermissions(String[] permissions) { int result;
//스트링 배열에 있는 퍼미션들의 허가 상태 여부 확인 for (String perms : permissions){
result = ContextCompat.checkSelfPermission(this, perms);
if (result == PackageManager.PERMISSION_DENIED){ //허가 안된 퍼미션 발견 return false; } }
//모든 퍼미션이 허가되었음 return true; }
@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("앱을 실행하려면 퍼미션을 허가하셔야합니다."); } break; } }
@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(PERMISSIONS, PERMISSIONS_REQUEST_CODE); } }); builder.setNegativeButton("아니오", new DialogInterface.OnClickListener() { public void onClick(DialogInterface arg0, int arg1) { finish(); } }); builder.create().show(); }
} |
3-6. javah를 사용하여 자바 코드에서 선언한 네이티브 메소드에 대응하는 C/C++ 함수가 선언되어 있는 헤더 파일을 jni 디렉토리에 자동으로 생성합니다.
우선 javah를 실행하기 편하게 안드로이드 스튜디오의 외부 도구로 등록합니다. 메뉴에서 File > Settings를 선택합니다.
Settings 창의 왼쪽 목록에서 Tools > External Tools를 선택하고 오른쪽에서 초록색 +를 클릭합니다.
아래처럼 각 항목의 내용을 채웁니다.
◼ Name
javah
◼ Description
Android Tool - javah
◼ Advanced Options를 클릭하고 다음 두 항목을 체크
Make console active on message in stdout
Make console active on message in stderr
◼ Program
Android Studio에서 번들로 제공되는 OpenJDK를 사용하면 program에 다음처럼 입력합니다.
C:\Program Files\Android\Android Studio\jre\bin\javah.exe
Oracle의 JDK를 사용한다면 program에 다음처럼 입력합니다. 사용하는 jdk 버전에 따라 디렉토리 위치가 다를 수 있습니다.
C:\Program Files\Java\jdk1.8.0_131\bin\javah.exe
◼ Arguments :
-v -jni -d $ModuleFileDir$/src/main/jni $FileClass$
◼ Working directory:
$SourcepathEntry$
다 입력하고나서 OK를 클릭하여 설정을 저장합니다.
External Tools에 javah가 추가되었습니다.
MainActivity를 선택하고 마우스 우클릭 후 보이는 메뉴에서 External Tools > javah를 선택합니다.
문제 없이 진행되면 다음과 같은 메시지가 출력됩니다.
"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
[Creating file RegularFileObject[C:\Users\webnautes\AndroidStudioProjects\UseOpenCVwithndkbuild\app\src\main\jni\com_tistory_webnautes_useopencvwithndk_build_MainActivity.h]]
Process finished with exit code 0 |
코드에 한글이 포함된 경우 다음과 같은 메시지들이 같이 출력되는데 결과를 얻는데 상관없으므로 무시하셔도 됩니다.
Error: unmappable character for encoding MS949 |
app / src / main 디렉토리 아래에 jni 디렉토리가 생성되고 그 안에 패키지 이름이 포함된 헤더 파일(여기엔선 com_tistory_webnautes_useopencvwithndk_build_MainActivity.h)이 생성됩니다.
현재 android 뷰이기 때문에 app / cpp디렉토리에 위치한 것처럼 보입니다.
자바 코드의 패키지 이름(com.tistory.webnautes.useopencvwithndk_build)과 액티비티 이름(MainActivity)의 조합으로 헤더 파일의 이름이 결정됩니다.
3-7. 자바에서 선언한 네이티브 메소드를 위한 C/C++ 구현을 cpp / main.cpp 파일에 작성합니다.
main.cpp는 공유 라이브러리 파일로 컴파일 되어 자바 코드에서 로드되어 사용됩니다.
cpp 디렉토리를 선택한 후, 마우스 우클릭하여 메뉴에서 New > File을 선택합니다.
새로 생성할 파일 이름으로 main.cpp를 적고 OK를 클릭합니다.
cpp에 main.cpp 파일이 생성됩니다.
main.cpp 파일에 다음 헤더파일 2개를 추가합니다.
두 번째 헤더파일은 앞에서 생성한 헤더파일 이름을 적어줍니다.
#include <jni.h> #include "com_tistory_webnautes_useopencvwithndk_build_MainActivity.h" |
두번째 헤더파일을 열어보면 JAVA에서 선언한 네이티브 메소드에 대응하는 JNI 함수가 다음 처럼 정의되어 있습니다.
/* 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: ConvertRGBtoGray * Signature: (JJ)V */ JNIEXPORT void JNICALL Java_com_tistory_webnautes_useopencvwithndk_1build_MainActivity_ConvertRGBtoGray (JNIEnv *, jobject, jlong, jlong);
#ifdef __cplusplus } #endif #endif |
생성된 JNI 함수를 main.cpp 파일에 복사해오고(파란색 부분) 추가로 필요한 코드를( 노란 부분) 추가합니다.
#include <jni.h> #include "com_tistory_webnautes_useopencvwithndk_build_MainActivity.h"
#include <opencv2/opencv.hpp>
using namespace cv;
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, COLOR_RGBA2GRAY);
} }
|
3-8. ndk-build를 사용하여 공유 라이브러리(.so)를 빌드하기 위해서는 Android.mk 파일과 Application.mk 파일을 작성해줘야 합니다.
cpp 디렉토리를 선택한 후, 마우스 우클릭하여 메뉴에서 New > File을 선택합니다.
새로 생성할 파일 이름으로 Android.mk를 적고 OK를 클릭합니다.
Text가 선택된 상태에서 OK를 클릭합니다.
cpp 에 Android.mk 파일이 생성됩니다.
Android.mk 파일에 다음 내용을 추가합니다.
공유 라이브러리를 생성할 경우에는 사용되는 외부 라이브러리 정보와 직접 작성한 C/C++ 소스코드 관련 정보를 입력합니다.
machine-woong님이 지적해주셨습니다. 진행하실때 OPENCVROOT 위치가 실제 Android용 OpenCV가 설치된 경로인지 확인을 하세요.
현재는 프로젝트 폴더 내의 opencv 폴더에 존재합니다.
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS)
#opencv library OPENCVROOT:= C:\Users\webnautes\AndroidStudioProjects\UseOpenCVwithndkbuild\opencv OPENCV_CAMERA_MODULES:=on OPENCV_INSTALL_MODULES:=on OPENCV_LIB_TYPE:=SHARED include ${OPENCVROOT}\native\jni\OpenCV.mk
LOCAL_MODULE := native-lib LOCAL_SRC_FILES := main.cpp LOCAL_LDLIBS += -llog
include $(BUILD_SHARED_LIBRARY) |
Android.mk를 생성한 방법대로 cpp에 Application.mk 파일도 TEXT 파일로 생성합니다.
Application.mk 파일에 다음 내용을 추가합니다. 컴파일시 사용되는 여러 변수들을 정의합니다.
APP_OPTIM := debug APP_ABI := arm64-v8a armeabi-v7a x86 x86_64 APP_PLATFORM := android-28
APP_STL := c++_static APP_CPPFLAGS := -frtti -fexceptions NDK_TOOLCHAIN_VERSION := clang
APP_BUILD_SCRIPT := Android.mk |
3-9. app 모듈의 build.gradle에 ndk-build를 사용하여 C/C++ 빌드가 이루어지기 위해 필요한 내용을 추가해줍니다. (노란색 줄)
android { compileSdkVersion 28 defaultConfig { applicationId "com.tistory.webnautes.useopencvwithndk_build" minSdkVersion 15 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } externalNativeBuild { ndkBuild { path 'src/main/jni/Android.mk' } }
} |
3-10. Sync Now를 클릭하여 Gradle build를 시작합니다.
문제 없으면 왼쪽 아래에 Gradle build finished라는 메시지가 보입니다.
빌드 후, 안드로이드 폰에 설치하여 실행시켜 보면 안드로이드폰의 방향에 따라 카메라 영상도 같이 회전합니다.
4. 참고
Android Studio에서 안드로이드 프로젝트 생성시 Include C++ Support 체크박스를 설정한 경우 생성되는 소스 코드
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