본문 바로가기

Programming/Android

Android studio 2.2에서 NDK 적용하기

[출처] : http://crystalcube.co.kr/183

밑에서도 언급하겠지만, 내가 테스트 해보니 다음과 같이 설정해줘야 한다.

CMakeLists.txt에 'set_target_properties'에 라이브러리 경로를 추가할 때 

꼭 'jniLibs'라는 폴더로 명시해야 한다.

add_library( NativeTuner SHARED IMPORTED)


set_target_properties( NativeTuner

                       PROPERTIES IMPORTED_LOCATION

                       ../../../../src/main/jniLibs/${ANDROID_ABI}/libNativeTuner.so)


target_link_libraries( # Specifies the target library.

                       native-lib


                       NativeTuner


                       # Links the target library to the log library

                       # included in the NDK.

                       ${log-lib} )


그리고 당연히! 그 위치에 해당 라이브러리들을 복사해줘야겠지?



=====================================================================


Android Studio 2.2 에서 NDK 빌드하기

(외부 prebuilt 파일 추가하기 .so)




Android Studio 는 너무나 자주 바뀐다


Android Studio 를 3년 넘게 쓰고있는데, 매번 느끼는거지만 너무 자주 바뀝니다. -_-;

물론 자주 바뀌는건 좋다고 생각합니다. 상황과 시대가 바뀌므로 그에 맞게 변화하는건 좋은 것이지요.

다만,, 개발자 입장에서는 너무나 괴롭습니다.


저는 프로젝트를 처음 시작할때에는 알고있는 것도,

새로운 트렌드나 변경된게 있는지 찾아보는 습관을 가지고 있습니다.



이번에 Android 에서 OpenAL 관련하여 ndk 빌드를 할 일이 생겼습니다.

마침 몇달전 Android Studio 2.2 (Stable Version)로 버전업이 되면서, ndk 관련 수정된게 있다는 소식을 듣고 있던터라,,

ndk 빌드관련해서 검색해 보았습니다.



역시나.... 새로운 feature 가 추가되었습니다.





CMakeLists.txt 를 사용하자


Android Studio는 기존에 ndk 를 빌드하는 방식이 이미 여러차례 바뀐 경력을 가지고 있습니다.

이번엔 Android Studio 에서 본격적으로 c++ 빌드를 지원하는데요.

ndk 빌드를 위해서 CMakeLists.txt 를 지원합니다.(cmake)


전에 Andriod.mk 파일을 쓰던 방식이 바뀌었단 이야깁니다.


관련된 문서는 아래 url 에서 확인 가능합니다.


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




간략히 환경설정에 대해서 짚고 넘어가겠습니다.





환경설정


1. 먼저 Android Studio 를 2.2 이상으로 업데이트 합니다.

2. SDK Tools 에서 아래 3가지를 항목을 찾아서 추가로 설치합니다.

1. LLDB

2. CMake

3. NDK

3. 이제 Android Studio 에서 새 프로젝트를 생성하면, 아래처럼 C++ 항목이 하나 추가됨을 알 수 있습니다.


"Include C++ Support" 를 체크하고, 프로젝트를 생성하면, 기본적인 CMakeLists.txt 파일과 /src/main/cpp 폴더아래 native-lib.cpp 가 있는것을 확인 할 수 있습니다.


gradle 파일을 열어보면, externalNativeBuild 의 프로퍼티가 추가된 것도 확인할 수 있습니다.



이렇게 총 3가지가 크게 추가/변경되었습니다.

1. "cpp" 폴더가 있음.

2. "CMakeLists.txt" 가 있음.

3. gradle 에 "externalNativeBuild -> cmake" 속성이 추가됨.






외부 Library 파일(.so) 추가하여 build 하기


위 내용까지 빌드+실행은 매우 잘 됩니다.


그러나~!!!!!!

외부 빌드된 library 파일(.so)을 추가해서 cpp 빌드를 하려고 하면,,

거지같은 상황에 부딪히게 됩니다.


도대체 이 .so 파일을 어떻게 추가해야 하는가???

하라는대로 다 했는데도.. 도통 reference 를 찾지 못합니다.


여기서 빡치기 시작합니다.

자료를 찾으려고 해도, 불과 길어야 두달? 정도 된 내용이라....

정보가 없습니다 -_-;;


어쩔수 없이 삽질해야 합니다.



아무튼 수많은 착오끝에 빌드에 성공하였습니다.

기본적인 CMakeLists.txt 에 대한 내용은 아래 사이트에 나와 있습니다.



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



해 보세요.

더럽게 안됩니다. ㅎ.




아무튼 결론만 적겠습니다.




1. .so 파일을 적당한 위치에 넣습니다.

 - 제 경우에는 src/main/cpp 아래 jniLibs 폴더를 만들고 그 안에 각 ABI 에 맞게 so 파일을 넣었습니다.

 - 말씀드렸듯이 '적당한 곳'에 넣으시면 됩니다. 기존 방식대로 jniLibs 를 java 와 동일한 레벨에 넣어도 됩니다.


2. .so 에 대한 header 파일을 적당한 위치에 넣습니다.

 - 제 경우에는 src/main/cpp 아래에 넣었습니다.


3. CMakeLists.txt 파일에 아래를 추가 합니다.

add_library( openal-lib

             SHARED

             IMPORTED )


set_target_properties(openal-lib

                       PROPERTIES IMPORTED_LOCATION

                       ../../../../src/main/cpp/jniLibs/${ANDROID_ABI}/libopenal.so)


include_directories( src/main/cpp/AL )


4. 그리고 target_link_libraries 라는 곳에 openal-lib 를 추가해 줍니다.

target_link_libraries( # Specifies the target library.

                        native-lib

                        openal-lib

                       # Links the target library to the log library

                       # included in the NDK.

                       ${log-lib} )



여기서 openal-lib 를 제가 정한 이름입니다. 여러분 상황(.so 라이브러리)에 맞게 바꾸시면 됩니다.

욕나오는 부분은..


위에 빨간색 부분입니다.

경로가 저렇더라고요. -_-+


왜 다른 파일들은 모두 root 가 app module 기준인데, 외부 so 파일은 저런 이상한 곳이 root 인지 모르겠습니다.

아니면 속 편하게, 혼자개발 하실거라면~ full path 를 적어주시면 됩니다.


E:/Development/Android/src/main/cpp/jniLibs/${ANDROID_ABI}/libopenal.so

이런식으로요.



추가로 말씀드릴 부분은 저처럼 모든 ABI 에 대해서 so 파일이 없는 경우에는 gradle 에서 abiFilters 를 통해 존재하는 ABI 만 걸어주시면 됩니다. 이 부분은 안드로이드 메뉴얼에서 읽어보세요~



마지막으로..여러분 이해를 돕기위해서 스샷 올려드립니다.

요즘들어 포스팅이 워낙 귀찮아서;;; 뒤죽박죽에 대충대충이네요 ㅎ.




출처: http://crystalcube.co.kr/183 [유리상자 속 이야기]





[출처] : http://webnautes.tistory.com/1054

Android Studio에서 NDK 지원하는 안드로이드 프로젝트를 생성한 후,  CMake를 사용하여 안드로이드 프로젝트에서 OpenCV 라이브러리를 사용할 수 있도록 설정하는 과정을 설명합니다.

 

 

 

테스트 환경은 다음과 같습니다.

안드로이드 스튜디오와 SDK를 다음처럼 글 작성 시점의  최신버전으로 업데이트 후 진행했습니다.

 

  • Windows 10

  • OpenCV 3.2

  • Android Studio 2.3.3

  • Android 8.0 (API 26)

 

 

 

다음 순서로 설명합니다.

 

1. NDK 지원하는 안드로이드 프로젝트 생성 및 테스트



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



3. CMake 사용한 NDK + OpenCV 예제 프로젝트



4. 빌드시 발생할 수 있는 에러 및 해결방법



5. 참고

 

 

1. NDK 지원하는 안드로이드 프로젝트 생성 및 테스트

 

1. 안드로이드 스튜디오에서  Include C++ Support 체크박스를 설정하고 새로운 프로젝트를 생성합니다.

 

 

 

 

2. Android 8.0 (API 26)에서는  Minimum SDKAPI 14로 해야 appcompat-v7라이브러리를 사용할 수 있습니다.

 

 

 

 

3.  프로젝트에 추가할 액티비티로 Empty Activity를 선택합니다.

 

 

 

 

4. 디폴트값으로 두고 다음 단계로 넘어갑니다.

 

AppCompatActivity 사용하기 위해  “Backwards Compatibility (AppCompat)”를 체크한 상태로 두었습니다.

 

 

 

 

5.  항목 두 개 모두 체크하고 Finish 버튼을 클릭하면 프로젝트가 생성됩니다.

 

 

 

 

6. 기존 안드로이드 프로젝트와 차이나는 부분은 두 군데입니다.

 

C/C++ 네이티브 코드를 저장하는 cpp 디렉토리가 추가되었으며,( / app / src / main / cpp )

네이티브 코드를 빌드하기 위해 사용되는 CMakeLists.txt(=CMake 스크립트)가 추가되었습니다.

( / app / CMakeLists.txt )

 

 

 

 

7.  현재 프로젝트에서 필요로하는 NDK가 설치안되어 있다면 프로젝트 생성 후, 다음과 같은  에러가 납니다.

 

 

 

안드로이드 스튜디오에서 CMake를 사용하여 C/C++ 코드를 컴파일 및 디버그하기 위해서는 다음 3가지가 필요합니다.

 

  • The Android Native Development Kit (NDK)

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

  • CMake

C/C++ 코드를 컴파일하여 네이티브 라이브러리 파일로 만들기 위해 사용됩니다.

  • LLDB

C/C++ 코드를 디버그하기 위해 사용되는 디버거입니다. 설치해주면 예전에 잡히지 않았던 에러나

           예외상황이 검출되며 에러 발생한 C/C++ 코드 위치를 알려줍니다.

 

 

안드로이드 스튜디오 메뉴에서 Tools > Android > SDK Manager를 선택합니다.

SDK Tools 탭을 선택한 후  CMake, NDK, LLDB를 선택하고 Apply 버튼을 클릭합니다.

 

 

 

 

OK를 클릭하면 다운로드 및 설치가 진행됩니다.

 

 

새로 설치한 CMake를 프로젝트에서 인식하도록 툴바에서 Sync Project   아이콘을   클릭합니다.

 

 

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

 

1. https://github.com/opencv/opencv/releases 에서 안드로이드를 위한 OpenCV 라이브러리( opencv-3.2.0-android-sdk.zip)를 다운로드 받습니다.

압축을 풀은 후, OpenCV-android-sdk 디렉토리를 적당한 곳에 복사해줍니다.  

여기서는 OpenCV-android-sdk 디렉토리를 C:\로 복사한 것으로 하고 진행하겠습니다.

 

 

 

 

2. 안드로이드 스튜디오 메뉴에서 File > New > Import Module를 선택합니다.

 

Source directory 입력란 옆에 있는 버튼을 클릭하여 OpenCV-android-sdk 디렉토리 하위에 있는 sdk\java를 선택합니다.

정상적으로 경로가 추가되었다면 Module name openCVLibrary320이 입력됩니다.

 

 

 

 

Finish 버튼을 클릭합니다.

 

 

 

3. Project 패널 Android 뷰인 상태에서는  안드로이드 프로젝트에 추가된  openCVLibrary320 모듈을 바로 확인할 수 없습니다.

 

 

 

왜냐면 openCVLibrary320 모듈에서 필요로하는 Android 4.0(API 14) SDK Platform 패키지가 설치안되어 있어서난 에러 때문에 보이지 않는 것입니다.

게다가 기존에 보이던 app 모듈의 manifests, java, res 항목도 에러때문에 같이 안보이게 되버립니다.

 

 

 

Error:Failed to find target with hash string 'android-14' in: C:\Users\note\AppData\Local\Android\Sdk
Install missing platform(s) and sync project

 

 

Install missing platform(s) and sync project를 클릭하여 Android 4.0(API 14) SDK Platform를 설치해주고 이어서  또 요구하는 Android SDK Build-Tools 25.0.0을 설치해주면  해결이 되긴 합니다.

 

하지만 권장사항대로 최신 SDK로 변경해주는 게 좋을 듯합니다.

 

 

Project 패널 Project 뷰로 변경하면 openCVLibrary320 모듈이 추가된 것을 확인할 수 있습니다.

 

 

 

4. app 모듈 build.gradle의  buildToolsVersion, compileSdkVersion, minSdkVersion, targetSdkVersion 값으로 openCVLibrary320 모듈 build.gradle를 수정해야 합니다.

 

 

왼쪽이 기존 app 모듈에서 사용중인 버전입니다.

아래처럼 대응되는 항목의 값을 openCVLibrary320 모듈에 똑같이 입력해주면 됩니다.

 

4. 변경사항을 프로젝트에 적용하기 위해 코드 편집기 오른쪽 위에 보이는 노란색줄에 있는 Try Again을 클릭합니다.

 

 

 

 

문제없으면 왼쪽 아래에 Gradle build finished라는 메시지가 보입니다.

 

 

 

 

5. 프로젝트의 디폴트 모듈(app)에서 openCVLibrary320 모듈을 사용하도록 설정해줘야 합니다.

 

메뉴에서 File > Project structure를 선택한 후,  왼쪽에 있는 리스트에서  Module 하위 항목인 app를 선택합니다.

 

 

 

 

Dependencies 탭을 선택하고 오른쪽 상단에 위치한 초록색 +를 클릭합니다.

 

 

 

 

Module dependency를 선택합니다.

 

 

 

 

추가했던 openCVLibrary320 모듈이 보입니다. OK버튼을 클릭합니다.

 

 

 

 

openCVLibrary320 모듈이 추가되었습니다. 이제 OK 버튼을 클릭하여 Project Structure 창을 닫습니다.

 

 

 

 

6. Gradle build가 시작되는데 문제 없으면 왼쪽 아래에 Gradle build finished라는 메시지가 보입니다.

 

 

 

 

7. C:\OpenCV-android-sdk\sdk\native에 위치한 libs 디렉토리를 안드로이드 프로젝트의 app\src\main에 복사해줍니다.  

 

그리고 디렉토리 이름을 JniLibs로 변경합니다.  변경하지 않으면 문제가 발생합니다.

안드로이드 스튜디오의 Project 패널에서 추가 된것을 확인 할 수 있습니다.

 

 

 

 

3. CMake 사용한 NDK + OpenCV 예제 프로젝트

 

1. AppCompat에서 타이틀바를 없애기 위해서는 res / values / styles.xml 파일을 다음 내용으로  대체해야 합니다.  

 

<resources>

<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);
setContentView(R.layout.activity_main);

 

참고 http://commin.tistory.com/63

 

 

 

2. 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. 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.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"

 

 

 

4.  JNI(Java Native Interface)를 사용하여 C/C++ 함수를 호출하는  JAVA 코드를 작성합니다.

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

 

package com.tistory.webnautes.useopencvwithcmake;

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_java3");
       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();
       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();
   }


}

 

 

 

5. 자바 코드의  ConvertRGBtoGray 네이티브 메소드가 에러로 표시됩니다..

마우스 커서를 가져가면 다음과 같은 메시지를 볼 수 있습니다.  

 

 

 

자바에서 선언한 ConvertRGBtoGray 네이티브 메소드에 대응되는 C/C++로 작성된 JNI 함수가 프로젝트에 없다는 에러입니다.

 

Cannot resolve corresponding JNI function Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_ConvertRGBtoGray less... (Ctrl+F1)
Reports native method declarations in Java where no corresponding JNI function is found in the project.

 

 

 

ConvertRGBtoGray 네이티브 메소드에 마우스 커서를 가져갔을때 보이는  빨간색 전구를 클릭하고

 

 

 

 

Create function을 선택합니다.

 

 

 

 

native-lib.cpp 파일에 JNI 함수가 자동으로 생성됩니다.

 

 

 

 

6. native-lib.cpp 파일에서 안드로이드 프로젝트 생성시  추가된 함수를 제거하고  opencv 라이브러리를 사용하기 위해 필요한 헤더파일을 추가합니다.

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;

 

 

JNI 함수 주변에 extern “C” {   }를 해줘야 합니다.

 

 

 

8. 입력 RGBA 이미지를 GRAY 이미지로 변환하는 코드도 추가합니다.

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

cvtColor(matInput, matResult, CV_RGBA2GRAY);

 

 

 

 

아직은  OpenCV 라이브러리의 헤더파일을 아직 안드로이드 프로젝트에서 인식 못해서 빨간색으로 보이지만 다음 단계를 해주면 해결됩니다.

 

 

7.   CMake 스크립트  app\CMakeLists.txt 파일을 열어 아래 내용처럼 수정합니다.

 

pathOpenCV에  OpenCV 라이브러리 경로, 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(pathOPENCV C:/OpenCV-android-sdk)
set(pathPROJECT C:/Users/note/AndroidStudioProjects/UseOpenCVwithCMake)
set(pathLIBOPENCV_JAVA ${pathPROJECT}/app/src/main/JniLibs/${ANDROID_ABI}/libopencv_java3.so)

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

include_directories(${pathOPENCV}/sdk/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).
            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} )

 

 

 

8. 코드 편집기 오른쪽 상단에 있는 Sync Now를 클릭합니다.

 

 

 

문제 없으면 왼쪽 아래에 Gradle build finished 메시지가 보이고 native-lib.cpp 파일에 있던 에러들도 사라집니다.

 

 

 

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

 

 

 

 

회전하고 나서 버벅거리는 현상이 좀 있습니다.  

(최근에 나온 NDK 15.0.4075724를 사용시 이런 현상이 사라졌습니다.

API 25, API 26를 사용하여 안드로이드 OS 5.0과 안드로이드 OS 7.0 폰에서 테스트해봤습니다.)

 

에뮬레이터와  일부 안드로이드폰에서는 동작시 문제가 있는 듯합니다.

 

4. 빌드시 발생할 수 있는 에러 및 해결방법

 

1. CMakeLists.txt 파일에서 OpenCV 라이브러리 경로가 틀린 경우에 발생할 수 있는 에러입니다.

 

Error:FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:externalNativeBuildDebug'.
> Build command failed.
Error while executing process

………………………………….

C:\Users\note\AndroidStudioProjects\UseOpenCVwithCMake\app\src\main\cpp\native-lib.cpp:3:10: fatal error: 'opencv2/core/core.hpp' file not found
 #include <opencv2/core/core.hpp>
          ^~~~~~~~~~~~~~~~~~~~~~~
 1 error generated.
 ninja: build stopped: subcommand failed.

 

 

C/C++ 네이티브 코드를 컴파일하기 위해 필요한 OpenCV 라이브러리의 헤더 파일을 찾지 못하게 됩니다.

set(pathOPENCV C:/OpenCV-android-sdk)

 

 

 

2. CMakeLists.txt 파일에서 안드로이드 프로젝트 경로가 틀린 경우에 발생할 수 있는 에러입니다.

 

mips64가 보이는 것은  본 포스팅에서 설명할 때 C:\OpenCV-android-sdk\sdk\native\libs에 있는 모든 CPU 아키텍처(ABI)를 위한 라이브러리 파일을 복사해왔기 때문입니다.

 

Error:error:
'C:/Users/note/AndroidStudioProjects/UseOpenCVwithCMake222/app/src/main/JniLibs/mips64/libopencv_java3.so',
needed by '../../../../build/intermediates/cmake/debug/obj/mips64/libnative-lib.so', missing and no known rule to make it

 

 

작성한 C/C++ 코드를 빌드하여 공유 라이브러리(libnative-lib.so)로 만들기 위해 필요한 libopencv_java3.so 파일의 위치를 찾을 수 없게 됩니다.

set(pathPROJECT C:/AndroidStudioProjects/OpencvTest)

 

 

안드로이드 프로젝트 빌드에 성공하면 JniLibs에 있는 각 CPU 아키텍처(ABI)별로 libopencv_java3.so 파일이 생성된 것을 볼 수 있습니다.

 

 

 

 

3. C:\OpenCV-android-sdk\sdk\native\libs 디렉토리를 안드로이드 프로젝트로 복사해온 후, 이름을 바꾸지 않고 진행하면 발생하는 에러입니다.

 

 

빌드 및 설치시에는 에러가 없습니다. 하지만  앱을 실행시키면 바로 중지되버립니다.

 

 

로그캣에서 다음과 같은 에러를 확인 할 수 있습니다.

 

java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file

"/data/app/com.tistory.webnautes.useopencvwithcmake-1/base.apk",

zip file "/data/app/com.tistory.webnautes.useopencvwithcmake-1/split_lib_dependencies_apk.apk",

zip file "/data/app/com.tistory.webnautes.useopencvwithcmake-1/split_lib_slice_0_apk.apk",

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

zip file "/data/app/com.tistory.webnautes.useopencvwithcmake-1/split_lib_slice_8_apk.apk",

zip file "/data/app/com.tistory.webnautes.useopencvwithcmake-1/split_lib_slice_9_apk.apk"],

nativeLibraryDirectories=[/data/app/com.tistory.webnautes.useopencvwithcmake-1/lib/arm64,

/data/app/com.tistory.webnautes.useopencvwithcmake-1/base.apk!/lib/arm64-v8a,

/data/app/com.tistory.webnautes.useopencvwithcmake-1/split_lib_dependencies_apk.apk!/lib/arm64-v8a,

/data/app/com.tistory.webnautes.useopencvwithcmake-1/split_lib_slice_0_apk.apk!/lib/arm64-v8a,

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

/data/app/com.tistory.webnautes.useopencvwithcmake-1/split_lib_slice_8_apk.apk!/lib/arm64-v8a,

/data/app/com.tistory.webnautes.useopencvwithcmake-1/split_lib_slice_9_apk.apk!/lib/arm64-v8a, /system/lib64, /vendor/lib64]]] couldn't find "libopencv_java3.so"
                                                    at java.lang.Runtime.loadLibrary0(Runtime.java:972)
                                                    at java.lang.System.loadLibrary(System.java:1530)
                                                    at com.tistory.webnautes.useopencvwithcmake.MainActivity.<clinit>(MainActivity.java:34)

 

 

libs 디렉토리를 다른 이름으로 변경하고 CMakeLists.txt 파일에서 pathLIBOPENCV_JAVA를 수정하면 해결됩니다.

set(pathLIBOPENCV_JAVA ${pathPROJECT}/app/src/main/JniLibs/${ANDROID_ABI}/libopencv_java3.so)

 

 

 

4. 자바 코드의 ConvertRGBtoGray 네이티브 코드에 대응하는 JNI 함수를 찾을 수 없어서 에러가 발생합니다.

 

앱을 실행시키면 바로 중지되며 로그캣에서 다음과 같은 에러를 확인 할 수 있습니다.

 

java.lang.UnsatisfiedLinkError: No implementation found for void com.tistory.webnautes.useopencvwithcmake.MainActivity.ConvertRGBtoGray(long, long)
(tried Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_ConvertRGBtoGray
and Java_com_tistory_webnautes_useopencvwithcmake_MainActivity_ConvertRGBtoGray__JJ)
                                                                                             
at com.tistory.webnautes.useopencvwithcmake.MainActivity.ConvertRGBtoGray(Native Method)
                                                                                             
at com.tistory.webnautes.useopencvwithcmake.MainActivity.onCameraFrame(MainActivity.java:131)

 

 

 

두가지 원인이 있는 듯합니다.

 

첫번째는 native-lib.cpp에 선언된 JNI 함수들 주변에  extern "C" { } 를  해주지 않아서 입니다.

 

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

}

 

 

 

두번째는 자바 코드의 ConvertRGBtoGray 네이티브 코드에 대응하는 JNI 함수가 구현되어 있지 않아서입니다.

 

본 포스팅의 CMake 사용한 NDK + OpenCV 예제 프로젝트 5번을 참고하여 해결하면 됩니다.

 

 

 

5. 참고

 

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


'Programming > Android' 카테고리의 다른 글

apk decompile 하기  (0) 2017.08.11
ffmpeg 3.2.2 with NDK build  (0) 2017.08.10
UI 없이 service를 broadcast 수신 받아 띄위기  (0) 2017.04.27
empty activity 만들기  (0) 2017.04.27
android studio에서 NDK 빌드하기  (0) 2016.08.11