在《Android Studio增加NDK代碼編譯支持--Mac環境》和《Mac平台下Opencv開發環境搭建》兩篇文章中,介紹了如何使用NDK環境和Opencv環境搭建與測試使用,現在,在PC端對圖像處理算法測試沒問題后,該在移動端進行功能移植了,ios平台的很簡單,直接把類庫拷進工程就行了,android的稍微麻煩點,這里就以android平台為例說明移植步驟。
為了更好的模塊移植,這里使用Android源碼的make文件寫法:*.mk,Android源碼是一個很大的工程,它的編譯采用一個大的mk文件,通過腳本文件的配置來自定義編譯的,在build/core/下面的Android.mk文件就是總的編譯文件入口:
這里寫的opencv安卓模塊也使用mk文件寫法來編譯so庫。這里新建了一個測試工程,可以在GitHub上download或fork來查看源碼: https://github.com/linjk/TestOpenCV
下面開始移植步驟:
1. 新建測試工程OpenCVTest:
2. 拷貝下載的opencv的android平台的開發包,這里下載3.1.0版本的:
這里把sdk目錄下的native目錄拷貝到工程根目錄,這個目錄下是c/c++語法的,java目錄是已封裝好的一些java接口,按需選擇吧,為了更好的算法移植而不用每次改寫,這里選擇native庫,復制后工程結構如下:
3. 新建jni目錄,用於編寫本地c++代碼:
在src目錄單擊右鍵,按下圖操作:
結果如下:
4. 編寫java類的本地接口聲明,用於給java層調用:
這里聲明一個很簡單的opencv本地方法,用於把一副圖像編程灰度圖像,當然,這個效果用安卓的圖像矩陣來處理就行了,但是,復雜一點的功能,如邊緣檢測、身份證識別就要借助opencv來弄了,這里僅做功能測試:
5. 生成本地方法橋接頭文件:
命令行進入src/main/java路徑,然后執行命令: javah -jni cn.linjk.jniBridge.OpenCVUtils, -jni參數后面參數格式是:包名+類名,結果如下:
我們把這個文件移動到jni目錄下,並新建一個同名的cpp類cn_linjk_jniBridge_OpenCVUtils.cpp:
6. 由於之前使用了android studio生成的jni目錄,因此,編譯上可能會和使用mk文件編譯生成so庫不一樣,這里取消它的路徑屬性:
在app/build.gradle文件的android塊下增加這個配置:
sourceSets{ main{ jni.srcDirs = [] } }
可以發現,jni目錄由藍色變成了黃色:
7. 編寫編譯規則文件,指定ndk路徑:
7.1 指定ndk路徑:
7.2 在jni目錄下新建兩個mk編譯文件,內容分別如下:
Android.mk文件內容如下:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) OpenCV_INSTALL_MODULES := on OpenCV_CAMERA_MODULES := off OPENCV_LIB_TYPE := SHARED ifeq ("$(wildcard $(OPENCV_MK_PATH))","") include ../../../../native/jni/OpenCV.mk else include $(OPENCV_MK_PATH) endif LOCAL_MODULE := testopencv LOCAL_SRC_FILES := cn_linjk_jniBridge_OpenCVUtils.cpp LOCAL_LDLIBS += -lm -llog -landroid include $(BUILD_SHARED_LIBRARY)
LOCAL_MODULE聲明的是模塊名稱,必須與在OpenCVUtils聲明的加載庫名一樣。
Application.mk文件內容如下:
APP_STL := gnustl_static APP_CPPFLAGS := -frtti -fexceptions APP_ABI := arm64-v8a armeabi armeabi-v7a mips mips64
APP_ABI聲明了聲明針對那些CPU架構的so庫。
7.3 在app/build.gradle聲明一個task,用於編譯生成so庫。
7.3.1 編輯jni的cpp文件,內容如下,為測試能否調用庫,這里先在函數打印cv版本:
// // Created by LinJK on 21/11/2016. // #include "cn_linjk_jniBridge_OpenCVUtils.h" #include <opencv2/opencv.hpp> #include <Android/log.h> #include <Android/asset_manager.h> #include <Android/asset_manager_jni.h> #define TAG "cn.linjk.opencvtest.jni" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) extern "C" { JNIEXPORT void JNICALL Java_cn_linjk_jniBridge_OpenCVUtils_img2gray (JNIEnv *env , jclass objClass, jstring imgFilePath) { LOGI("OpenCV version: %s", CV_VERSION); } }
7.3.2 增加生成so庫的task:
7.3.3 執行任務,生成so庫:
執行命令"gradle cv_ndkBuild",結果如下:
把對應得so庫復制到app/libs目錄下對應cpu架構目錄下:
8. 在MainActivity調用看能否輸出opencv庫版本:
MainActivity.java內容:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); OpenCVUtils.img2gray(""); } }
運行后,發現出現如下錯誤:
這是我們指定生成動態鏈接庫,因此,還需要libopencv_java3.so這個庫,復制到我們的libs下,再次運行,結果如下:
輸出正確,可以繼續下一步了。
附:
也可以使用命令“arm-none-eabi-readelf -d libtestopencv.so”查看其需要的鏈接庫,命令執行結果如下:
.so文件是ELF(Excutable and Linking Formar)格式的縮寫,最初由UNIX系統實驗室發布,它是應用程序二進制接口的一部分,ELF文件以節(section)的方式組織在一起,“節”描述了文件的各項信息,例如代碼、數據、符號表、重定位表、全局偏移表等。
9. 編寫圖像處理類:
9.1 這里使用照相機獲取輸入圖像,代碼看github的源碼就行,這里主要看看c++最終代碼:
cn_linjk_jniBridge_OpenCVUtils.cpp
// // Created by LinJK on 21/11/2016. // #include "cn_linjk_jniBridge_OpenCVUtils.h" #include <opencv2/opencv.hpp> #include <string> #include <iostream> #include <Android/log.h> #include <Android/asset_manager.h> #include <Android/asset_manager_jni.h> #define TAG "cn.linjk.opencvtest.jni" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) using namespace std; using namespace cv; class ImageUtils{ public: void imageToGray(Mat inputImg, string outFilePath); }; void ImageUtils::imageToGray(Mat inputImg, string outFilePath) { Mat gray; Mat input = inputImg.clone(); cvtColor(input, gray, COLOR_BGR2GRAY); imwrite(outFilePath, gray); } extern "C" { JNIEXPORT void JNICALL Java_cn_linjk_jniBridge_OpenCVUtils_img2gray (JNIEnv *env , jclass objClass, jstring imgFilePath) { LOGI("OpenCV version: %s", CV_VERSION); char buf[128]; const char *str = env->GetStringUTFChars(imgFilePath, 0); LOGD("圖像路徑: %s", str); Mat img = imread(str); if (!img.data) { LOGE("-----CV: 讀取相片數據出錯"); } else { LOGD("-----CV: 讀取相片數據成功"); ImageUtils().imageToGray(img, str); } } }
MainActivity.java方法內容如下:
package cn.linjk.opencvtest; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Environment; import android.provider.MediaStore; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.Display; import android.view.View; import android.widget.Button; import android.widget.ImageView; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import cn.linjk.jniBridge.OpenCVUtils; public class MainActivity extends AppCompatActivity { private Button btnOpenCamera; private ImageView ivImgOutput; private Button btnImgGray; private String imageFilePath; private static final int CAMERA_RESULT = 1112; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnOpenCamera = (Button)findViewById(R.id.btn_open_camera); btnOpenCamera.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View pView) { openCamera(); } }); ivImgOutput = (ImageView)findViewById(R.id.img_output); btnImgGray = (Button)findViewById(R.id.img_gray); btnImgGray.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View pView) { OpenCVUtils.img2gray(imageFilePath); // BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options(); bmpFactoryOptions.inJustDecodeBounds = true; Bitmap bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); bmpFactoryOptions.inSampleSize = calculateInSampleSize(bmpFactoryOptions, 1280, 800); bmpFactoryOptions.inJustDecodeBounds = false; bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); ivImgOutput.setImageBitmap(bmp); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options(); bmpFactoryOptions.inJustDecodeBounds = true; Bitmap bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); bmpFactoryOptions.inSampleSize = calculateInSampleSize(bmpFactoryOptions, 1280, 800); bmpFactoryOptions.inJustDecodeBounds = false; bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); ivImgOutput.setImageBitmap(bmp); saveBitmap(bmp); } } private void openCamera() { imageFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/testImage.jpg"; File imageFile = new File(imageFilePath); Uri imageFileUri = Uri.fromFile(imageFile); Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); i.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri); startActivityForResult(i, CAMERA_RESULT); } private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); inSampleSize = heightRatio < widthRatio ? widthRatio : heightRatio; } return inSampleSize; } private void saveBitmap(Bitmap bm) { File f = new File(imageFilePath); if (f.exists()) { f.delete(); } try { FileOutputStream out = new FileOutputStream(f); bm.compress(Bitmap.CompressFormat.PNG, 90, out); out.flush(); out.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
運行結果如下:
這里,opencv在android平台的移植和簡單功能測試已經完成了,后面更多精彩opencv算法就可以繼續實現啦~~
詳細代碼移步我的GitHub查閱即可:
https://github.com/linjk/TestOpenCV