在前面 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安装
这里直接使用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