在前面 Android專欄 中詳細介紹了如何在Android Studio中調用通過jni封裝的c++庫。
在Android使用 opencv c++代碼,需要準備opencv4android,也就是c++的任何代碼,是使用Android NDK編譯的,相當于在windows/mac上使用Android stdido交叉編譯。
本文再介紹服務端的使用方式,c++通過jni封裝的庫,直接被java后端服務代碼調用。 這里c++依賴庫都是linux主機上,jni有關庫也都是linux上的,因此就不存在交叉編譯。
最后,還將項目打包成 jar包。
實際項目參考 GitCode FLowMeasurem。
文章目錄
- 1、環境準備
- 1.1、java sdk安裝
- 1.2、opencv
- 1.3、gcc/g++ 和 cmake
- 2、項目實現
- 2.1、java端代碼
- 2.2、生成頭文件
- 2.4、C++實現
- 2.5、編譯共享庫
- 2.5.1、命令行編譯
- 2.5.2、cmake編譯
- 2.6、測試運行
- 3、導出Jar包給后端直接使用
- 3.1、項目結構
- 3.2、項目準備
- 3.2.1、創建Java類
- 3.2.2、生成JNI頭文件
- 3.2.3、實現c++代碼
- 3.2.4、編寫CMake構建文件
- 3.2.5、構建動態庫
- 3.3、 打包jar
- 3.3.1、編譯java代碼
- 3.3.2、創建MANIFEST.MF
- 3.3.3 打包JAR
- 3.3.4、jar包中添加so動態庫
- 3.4、測試jar包
- 3.5、包聲明問題
- 4、優化 Jar 包代碼和結構
1、環境準備
1.1、java sdk安裝
這里直接使用apt安裝(可能還需要配置環境變量),
sudo apt install openjdk-8-jdk
之后使用 javac
、javah
工具,能正常使用即可。
1.2、opencv
簡單起見,直接
sudo apt install libopencv-dev
1.3、gcc/g++ 和 cmake
不贅述。
2、項目實現
2.1、java端代碼
我們定義一個 OpenCVJNI.java
類,里面包含native函數,以及測試代碼main函數。
public class OpenCVJNI {// 加載本地庫static {System.loadLibrary("OpenCVJNI");}// 聲明本地方法public native int detectFaces(String imagePath, String outputPath);// 測試main函數public static void main(String[] args) {if (args.length < 2) {System.out.println("Usage: java OpenCVJNI <inputImage> <outputImage>");return;}OpenCVJNI ocv = new OpenCVJNI();int faceCount = ocv.detectFaces(args[0], args[1]);System.out.println("Detected " + faceCount + " faces.");}
}
2.2、生成頭文件
編譯Java類的命令 javac OpenCVJNI.java
此時,會在當前目錄生成 OpenCVJNI.class
文件;
繼續執行 javah -jni OpenCVJNI
會繼續在當前目錄生成 OpenCVJNI.h
文件。
我們可以使用 javac OpenCVJNI.java -h ./
直接在當前目錄生成class文件 ( -s
指定class保存目錄), 和 -h
指定目錄下保存生的 h 文件。
內容如下
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class OpenCVJNI */#ifndef _Included_OpenCVJNI
#define _Included_OpenCVJNI
#ifdef __cplusplus
extern "C" {
#endif
/** Class: OpenCVJNI* Method: detectFaces* Signature: (Ljava/lang/String;Ljava/lang/String;)I*/
JNIEXPORT jint JNICALL Java_OpenCVJNI_detectFaces(JNIEnv *, jobject, jstring, jstring);#ifdef __cplusplus
}
#endif
#endif
2.4、C++實現
#include <jni.h>
#include <opencv2/opencv.hpp>
#include "OpenCVJNI.h"using namespace cv;JNIEXPORT jint JNICALL Java_OpenCVJNI_detectFaces(JNIEnv *env, jobject obj, jstring imagePath, jstring outputPath) {// 將Java字符串轉換為C字符串const char* inputPath = env->GetStringUTFChars(imagePath, 0);const char* outPath = env->GetStringUTFChars(outputPath, 0);// 加載圖像Mat image = imread(inputPath);if(image.empty()) {env->ReleaseStringUTFChars(imagePath, inputPath);env->ReleaseStringUTFChars(outputPath, outPath);return -1;}// 轉換為灰度圖像Mat gray;cvtColor(image, gray, COLOR_BGR2GRAY);// 加載預訓練的人臉檢測器CascadeClassifier faceDetector;String faceCascadePath = "/usr/share/opencv4/haarcascades/haarcascade_frontalface_default.xml";if(!faceDetector.load(faceCascadePath)) {env->ReleaseStringUTFChars(imagePath, inputPath);env->ReleaseStringUTFChars(outputPath, outPath);return -2;}// 檢測人臉std::vector<Rect> faces;faceDetector.detectMultiScale(gray, faces, 1.1, 3, 0, Size(30, 30));// 在檢測到的人臉周圍繪制矩形for(size_t i = 0; i < faces.size(); i++) {rectangle(image, faces[i], Scalar(0, 255, 0), 2);}// 保存結果圖像imwrite(outPath, image);// 釋放資源env->ReleaseStringUTFChars(imagePath, inputPath);env->ReleaseStringUTFChars(outputPath, outPath);return faces.size();
}
2.5、編譯共享庫
2.5.1、命令行編譯
使用g++命令行編譯
g++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -I/usr/include/opencv4 \-shared -fPIC -o libOpenCVJNI.so OpenCVJNI.cpp \-lopencv_core -lopencv_imgproc -lopencv_objdetect -lopencv_highgui
若提示錯誤找不到jni有關頭文件,請配置環境變量 JAVA_HOME
, 例如這里的為 /usr/lib/jvm/java-8-openjdk-amd64
編譯之后,會在當前目錄下生成 libOpenCVJNI.so
文件。
2.5.2、cmake編譯
在當前目錄創建 CMakeLists.txt
:
cmake_minimum_required(VERSION 3.5)
project(OpenCVJNI)find_package(Java REQUIRED)
find_package(JNI REQUIRED)
find_package(OpenCV REQUIRED)include_directories(${JNI_INCLUDE_DIRS})
include_directories(${OpenCV_INCLUDE_DIRS})add_library(OpenCVJNI SHARED OpenCVJNI.cpp)
target_link_libraries(OpenCVJNI ${OpenCV_LIBS})
執行以下命令,在當build目錄下生成 libOpenCVJNI.so
文件。
mkdir build
cd build
cmake ..
make
2.6、測試運行
以cmake方式為例,給出當前項目目錄結構
OpenCVJNIProject/
├── CMakeLists.txt
├── lena.png
├── OpenCVJNI.java
├── OpenCVJNI.cpp
├── OpenCVJNI.h
└── build/└── libOpenCVJNI.so
我們運行時,需要將編譯生成的 libOpenCVJNI.so
,復制到Java庫路徑或者指定路徑,之后在OpenCVJNI.class的目錄下執行。
- 方式1
cd build
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
cd ..
java -Djava.library.path=. OpenCVJNI ../lena.png output.jpg
- 方式2
在項目目錄下指定so目錄path
java -Djava.library.path=./build OpenCVJNI ../lena.png output.jpg
- 方式3
將 OpenCVJNI.class 和 libOpenCVJNI.so 放在同一目錄(當前命令也包含圖片),直接運行
java OpenCVJNI ../lena.png output.jpg
3、導出Jar包給后端直接使用
前面OpenCVJNI.java
就包含了接口,也包含了測試代碼。 這里,我們將前面人臉識別的接口進行封裝成一個jar包,供其他java項目直接調用。
3.1、項目結構
我們按照java調用的格式創建一個目錄結構
MyOpenCVProject/
├── native/ # 本地代碼(C/C++)目錄
│ ├── CMakeLists.txt # C++構建配置
│ ├── src/ # C++源代碼
│ └── lib/ # 生成的動態庫
├── java/ # Java代碼目錄
│ ├── src/ # Java源代碼
│ └── target/ # 構建輸出
└── dist/ # 最終分發目錄
3.2、項目準備
3.2.1、創建Java類
在創建目錄 java/src/com/magicsky/OpenCVWrapper
,在當前包下創建OpenCVWrapper.java
文件.
package com.magicsky.OpenCVWrapper;public class OpenCVWrapper {static {System.loadLibrary("opencv_jni"); // 加載動態庫}// 聲明本地方法public native int detectFaces(String inputPath, String outputPath);// 輔助方法:獲取當前平臺對應的庫名稱private static String getLibraryName() {String osName = System.getProperty("os.name").toLowerCase();if (osName.contains("linux")) {return "opencv_jni";} else if (osName.contains("win")) {return "opencv_jni";} else if (osName.contains("mac")) {return "opencv_jni";}return "opencv_jni";}
}
3.2.2、生成JNI頭文件
首先編譯生成class文件,并導出頭文件,這里一步到位
cd java/src
javac -h ../../native/src com/magicsky/OpenCVWrapper/OpenCVWrapper.java
運行之后,會在 OpenCVWrapper.java
同級目錄下生成 OpenCVWrapper.class
文件。
類似Android中,在 native/src
下創建了一個文件 com_magicsky_OpenCVWrapper_OpenCVWrapper.h
。
3.2.3、實現c++代碼
在native/src/
下創建opencv_jni.cpp
代碼,除了引用目錄和函數命名,其他內容和前述 OpenCVJNI.java
內容一致。
#include <jni.h>
#include <opencv2/opencv.hpp>
#include "com_magicsky_OpenCVWrapper_OpenCVWrapper.h"using namespace cv;JNIEXPORT jint JNICALL Java_com_magicsky_OpenCVWrapper_OpenCVWrapper_detectFaces(JNIEnv *env, jobject obj, jstring imagePath, jstring outputPath) {// 實現代碼與之前示例相同// ...
}
3.2.4、編寫CMake構建文件
cmake_minimum_required(VERSION 3.5)
project(OpenCVJNI)find_package(Java REQUIRED)
find_package(JNI REQUIRED)
find_package(OpenCV REQUIRED)include_directories(${JNI_INCLUDE_DIRS})
include_directories(${OpenCV_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib)add_library(opencv_jni SHARED src/opencv_jni.cpp)
target_link_libraries(opencv_jni ${OpenCV_LIBS})
3.2.5、構建動態庫
cd native
mkdir -p build
cd build
cmake …
make
生成的動態庫會在native/lib/目錄下,名為libopencv_jni.so
。
查看項目目錄結構主要文件如下
3.3、 打包jar
有了以上so文件,按照jar包規則將需要的數據組織起來。
3.3.1、編譯java代碼
其實就是生成class文件。可以之前復制之前生成的。這里使用命令 javac -d
生成并存入指定位置。
cd java/src
javac -d ../target com/magicsky/OpenCVWrapper/OpenCVWrapper.java
運行后在 java/target 目錄下戶集成一個多級目錄,并創建文件 java/target/com/magicsky/OpenCVWrapper/OpenCVWrapper.class
。
-s
的結果為 java/target/OpenCVWrapper.class
3.3.2、創建MANIFEST.MF
創建 java/target/META-INF/
目錄,并添加 `MANIFEST.MF文件 ,內容如下
Manifest-Version: 1.0
Class-Path: .
3.3.3 打包JAR
先試用 jar 工具 打包 MANIFEST.MF 和 OpenCVWrapper.class ,命令如下
mkdir dist
cd java/target
jar cvfm ../../dist/OpenCVWrapper.jar META-INF/MAINFEST.MF com/
運行結果如下
$ mkdir dist
$ cd java/target/
$ jar cvfm ../../dist/OpenCVWrapper.jar META-INF/MAINFEST.MF com/
added manifest
adding: com/(in = 0) (out= 0)(stored 0%)
adding: com/magicsky/(in = 0) (out= 0)(stored 0%)
adding: com/magicsky/OpenCVWrapper/(in = 0) (out= 0)(stored 0%)
adding: com/magicsky/OpenCVWrapper/OpenCVWrapper.class(in = 829) (out= 507)(deflated 38%)
3.3.4、jar包中添加so動態庫
將動態庫打包到JAR中特定目錄(如native/linux-x86_64):
cd dist
mkdir -p native/linux-x86_64
cp ../native/lib/libopencv_jni.so native/linux-x86_64/jar uf OpenCVWrapper.jar native/linux-x86_64/libopencv_jni.so
打包好后下載到本地,解壓查看jar文件結構和內容
至此,jar打包完成。
還可以使用更高級的 Maven/Gradle 構建
3.4、測試jar包
準備目錄結構,并拷貝對應文件
$ tree
.
├── dist
│ ├── native
│ │ └── linux-x86_64
│ │ └── libopencv_jni.so
│ └── OpenCVWrapper.jar
├── lena.png
├── Main.java
在項目根目錄中添加 Main.java
文件,內容為
import com.magicsky.OpenCVWrapper.OpenCVWrapper;public class Main {public static void main(String[] args) {OpenCVWrapper wrapper = new OpenCVWrapper();int faceCount = wrapper.detectFaces("lena.png", "output.jpg");System.out.println("Detected " + faceCount + " faces.");}
}
使用 javac -cp dist/OpenCVWrapper.jar Main.java
編譯生成 Main.class
文件。
之后執行命令,需要指定so目錄, jar 目錄等 (根目錄下執行)
java -Djava.library.path=dist/native/linux-x86_64 -cp dist/OpenCVWrapper.jar:. Main
運行成功,截圖如下
3.5、包聲明問題
前面的測試代碼,Main.java在根目錄,不存在包聲明。下面結構中,存在一個test包。那么在Main.java文件第一行為 "package test;
"
$ tree
.
├── dist
│ ├── native
│ │ └── linux-x86_64
│ │ └── libopencv_jni.so
│ └── OpenCVWrapper.jar
├── lena.png
├── test
│ ├── Main.class
│ └── Main.java
有包聲明時,命令需要修改 (根目錄下執行,包聲明中最上一層)
java -Djava.library.path=./dist/native/linux-x86_64 -cp ./dist/OpenCVWrapper.jar:. test.Main
4、優化 Jar 包代碼和結構
前面jar包在使用時,沒有使用jar包中的 native/lib/libopencv_jni.so 文件,而是拷貝了一份運行時再指定路徑。
我們應該在用戶使用該jar時,解壓jar中的so并使用。
修改 java 文件如下 :
package com.magicsky.OpenCVWrapper;import java.io.*;
import java.nio.file.*;public class OpenCVWrapper {// static {// System.loadLibrary("opencv_jni"); // 加載動態庫// }static {loadLibrary();}private static void loadLibrary() {try {String libName = getLibraryName();String libPath = "/native/" + getPlatform() + "/lib" + libName + ".so";// 從JAR中提取庫到臨時目錄InputStream in = OpenCVWrapper.class.getResourceAsStream(libPath);if (in == null) {throw new RuntimeException("Native library not found in JAR: " + libPath);}Path tempDir = Files.createTempDirectory("native-lib");tempDir.toFile().deleteOnExit();Path tempLib = tempDir.resolve("lib" + libName + ".so");Files.copy(in, tempLib, StandardCopyOption.REPLACE_EXISTING);in.close();// 加載庫System.load(tempLib.toAbsolutePath().toString());} catch (IOException e) {throw new RuntimeException("Failed to load native library", e);}}// 聲明本地方法public native int detectFaces(String inputPath, String outputPath);// 輔助方法:獲取當前平臺對應的庫名稱private static String getLibraryName() {String osName = System.getProperty("os.name").toLowerCase();if (osName.contains("linux")) {return "opencv_jni";} else if (osName.contains("win")) {return "opencv_jni";} else if (osName.contains("mac")) {return "opencv_jni";}return "opencv_jni";}private static String getPlatform() {String osName = System.getProperty("os.name").toLowerCase();String osArch = System.getProperty("os.arch").toLowerCase();if (osName.contains("linux")) {return "linux-" + osArch;} else if (osName.contains("win")) {return "win-" + osArch;} else if (osName.contains("mac")) {return "mac-" + osArch;}throw new UnsupportedOperationException("Unsupported platform: " + osName + "/" + osArch);}
}
重新編譯并打包
cd java/src
javac -d ../target/ com/magicsky/OpenCVWrapper/OpenCVWrapper.java
cd ../target
jar cvfm ../../dist/OpenCVWrapper.jar META-INF/MAINFEST.MF com/
cd ../../dist
jar uf opencv-wrapper.jar native/linux-x86_64/libopencv_jni.so