上一篇文章講了如何配置OpenCV Java SDK 以及基本使用, 傳送門:OpenCV Java SDK
使用Java的好處是調用簡單, 不需要寫 JNI 相關代碼, 缺點是要安裝 Opencv Manager 或者打包一個10M左右的動態庫(libopencv_java3.so), 下面就講一下如何在 C++ 代碼中使用 OpenCV 以及編譯方式, 編譯方式也包括兩種: 動態鏈接和靜態鏈接.
以動態鏈接方式編譯
首先下載好OpenCV-android-sdk, 解壓, 然後自己新建一個JNI的項目, 我測試項目目錄結構如下:
├── jni
│ ├── Android.mk
│ ├── Application.mk
│ └── test.cpp
各個文件對應代碼如下:
Application.mk
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
#32位 CUP
APP_ABI := armeabi-v7a
# 64位 CPU
#APP_ABI := armeabi-v8a
APP_PLATFORM := android-14
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# 設置下載好的OpenCV-android-sdk路徑, 可以爲相對路徑和絕對路徑
OPENCV_ANDROID_SDK := /mnt/e/wsl/OpenCV-android-sdk
#OPENCV_LIB_TYPE := STATIC
# 以動態鏈接式編譯
OPENCV_LIB_TYPE := SHARED
# 引入OpenCV的配置文件
ifdef OPENCV_ANDROID_SDK
ifneq ("","$(wildcard $(OPENCV_ANDROID_SDK)/OpenCV.mk)")
include ${OPENCV_ANDROID_SDK}/OpenCV.mk
else
include ${OPENCV_ANDROID_SDK}/sdk/native/jni/OpenCV.mk
endif
else
include ../../sdk/native/jni/OpenCV.mk
endif
LOCAL_SRC_FILES := test.cpp
LOCAL_C_INCLUDES += $(LOCAL_PATH)
LOCAL_LDLIBS += -llog -ldl
# 解決編譯爲可執行文件運行時出現的錯誤:
# error: only position independent executables (PIE) are supported
LOCAL_CFLAGS += -pie -fPIE
LOCAL_LDFLAGS += -pie -fPIE
LOCAL_MODULE := blur_test
include $(BUILD_EXECUTABLE)
#include $(BUILD_SHARED_LIBRARY)
test.cpp
#include <iostream>
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
Mat src = imread(argv[1]);
if (src.empty()) {
cout << " data error" << endl;
}
medianBlur(src, src, 29);
imwrite("/sdcard/result.jpeg", src);
return 0;
}
- Android.mk 配置中
OPENCV_LIB_TYPE := SHARED
表明以動態鏈接方式編譯可執行文件, 即可執行文件運行時候, 還得依賴libopencv_java3.so
(在OpenCV-android-sdk/sdk/native/libs/目錄下) 這個算法庫. - test.cpp 中通過讀入一張圖片, 然後調用 OpenCV mediaBlur 進行模糊處理, 最後將結果轉爲jpeg圖片寫到 sdcard 目錄下面.
- 編譯過程中會有WARNING, 因爲我們沒有指明依賴的 libopencv_java.so , 可忽略, 在 jni 目錄下執行
ndk-build
(需先裝好 NDK 環境), 編譯的可執行文件在上級目錄的libs
文件夾下面,編譯完成後 blur_test 可執行文件本身只有幾百KB
下面push對應文件並運行:
// 可執行文件
adb push ../libs/armeabi-v7a/blur_test /system/bin/
// 依賴的opencv 算法庫
adb push /mnt/d/download/android/OpenCV-android-sdk/sdk/native/libs/armeabi-v7a/libopencv_java3.so /system/lib/
//測試用的圖片
adb push /mnt/c/Users/Administrator/Desktop/test.jpg /sdcard/
//運行可執行文件
adb shell /system/bin/blur_test /sdcard/test.jpg
效果圖:
以靜態鏈接方式編譯
以靜態鏈接方式編譯, 只需做如下修改:
- 將 Android.mk 中
OPENCV_LIB_TYPE := SHARED
改爲OPENCV_LIB_TYPE := STATIC
- 不需要額外的
libopencv_java3.so
修改完後執行 ndk-build
即可, 此時可執行文件大小爲 3MB 左右, 此時運行可執行文件就不需要 libopencv_java3.so
.
可見動態鏈接和靜態鏈接區別是, 一個是運行時加載其他庫中依賴的代碼, 一個是直接將所有需要的代碼編譯到當前模塊中, 各有優劣.
通過JNI調用
上面爲了演示方便, 都是將代碼編譯爲可執行文件 push 到手機中運行的, 實際使用過程中,
都是編譯爲動態庫(.so)通過JNI調用的, JNI部分比較常見, 就不貼代碼了,基本操作步驟如下:
- 編寫JNI接口, 修改Android.mk中最後的
include $(BUILD_EXECUTABLE)
改爲include $(BUILD_SHARED_LIBRARY)
, JNI編寫參考我之前寫的教程 JNI註冊兩種方式 - 在Java代碼中, 由於圖片都是Bitmap, 需將 Bitmap 中的圖片數據傳到 JNI 層, 一般有如下兩種方法:
- 利用 Bitmap 方法
copyPixelsToBuffer(Buffer dst)
將像素數據複製到ByteBuffer
中, 然後將ByteBuffer
中的數組或者ByteBuffer
對象通過JNI傳到native層, 然後處理, 處理完後通過Bitmap方法copyPixelsFromBuffer(Buffer src)
將數據複製回來即可, 但這種方法效率低, 佔用額外內存, 不推薦. - 直接使用 Bitmap 的 NDK 接口來操作 Bitmap 數據, 基本做法就是通過 JNI將 Bitmap 對象傳到 native 層, 然後通過 NDK 提供的接口進行操作, 這部分代碼網上有很多示例, 可自行搜索.
- 不管是通過何種方式將 Bitmap 裏的數據傳到C++中, 最終我們都是得到一個
unsigned char*
類型的數組, 然後我們通過 OpenCV 中 Mat 的構造函數將數據轉爲可操作的 Mat 對象:
Mat::Mat(int _rows, int _cols, int _type, void* _data, size_t _step);
// 因爲Bitmap默認是ARGB格式, 4個通道, 所以爲CV_8UC4,
// imageData表示我們傳下來的圖像數組數據, 最後參數 0, 表示AUTO_STEP,
// 即OpenCV內部自動計算取數據的步長
Mat image(height, width, CV_8UC4, imageData, 0);
- 得到Mat對象後, 其他操作就非常簡單易懂了, 和 PC 端 OpenCV 使用方式一樣.
使用何種編譯方式
這個選擇起來比較簡單, 不管以何種方式編譯, 目的是爲了減小App體積, 所以可以兩種方式都試下,看看哪種方式最終動態庫體積最小, 就選用哪種方式.