Linux C++ JNI封装、打包成jar包供Java调用详细介绍

在前面 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

之后使用 javacjavah 工具,能正常使用即可。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/79148.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

4.1 模块概述

1.Python结构 工程 > 包 > 模块 Python工程: “Python项目中最大的文件夹(本质就是一个文件夹)” --- 左侧的 CODE文件夹 为Python工程 Python包: 本质就是一个文件夹,但是python包中具备具体的标识,如果没有标识则不能导入 --- 左侧的 01.Python基础 文件夹为python包 P…

AJAX 实例

AJAX 实例 引言 Ajax&#xff08;Asynchronous JavaScript and XML&#xff09;是一种在无需重新加载整个页面的情况下&#xff0c;与服务器交换数据并更新部分网页的技术。Ajax通过在后台与服务器交换数据&#xff0c;实现了页面的动态更新&#xff0c;从而提高了用户体验和…

相机的基础架构

&#x1f4f7; 相机相关基础架构学习路径 一、了解手机相机系统架构 Android Camera HAL&#xff08;如果你是做 Android 平台&#xff09; 学习 Camera HAL3 架构&#xff08;基于 camera_device_t, camera3_device_ops 接口&#xff09; 熟悉 CameraService → CameraProvid…

MLX Chat - 基于 Streamlit 的 MLX 前端界面

本文翻译整理自&#xff1a;https://github.com/da-z/mlx-ui 一、关于 MLX Chat 一个基于 Streamlit 的简单 UI/网页前端&#xff0c;用于 MLX mlx-lm 项目。 相关链接资源 github : https://github.com/da-z/mlx-uiMLX 社区模型库&#xff1a;https://huggingface.co/mlx-co…

el-table 自定义列、自定义数据

一、对象数组格式自定义拆分为N列 1-1、数据格式&#xff1a; const arrayList ref([{"RACK_NO": "A-1-001"},{"RACK_NO": "A-1-002"},{ "RACK_NO": "A-1-003"},//省略多个{"RACK_NO": "A-1-100…

JVM 如何使用性能分析工具定位代码中的性能问题?

核心思想&#xff1a; 通过工具观察程序在特定负载下的运行状态&#xff0c;识别消耗资源最多的代码段&#xff08;热点代码&#xff09;、异常的内存分配模式或线程阻塞情况&#xff0c;然后针对性的优化代码。 通用步骤&#xff1a; 确定问题&#xff1a; 首先明确遇到了什…

Python虚假新闻检测识别

程序示例精选 Python虚假新闻检测识别 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《Python虚假新闻检测识别》编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。 学习与应…

网络原理 - 12(HTTP/HTTPS - 3 - 响应)

目录 认识“状态码”&#xff08;status code&#xff09; 200 OK 404 Not Found 403 Forbidden 405 Method Not Allowed 500 Internal Server Error 504 Gateway Timeout 302 Move temporarily 301 Moved Permanently 418 I am a teaport 状态码小结&#xff1a; …

Spring Boot中集成Guava Cache或者Caffeine

一、在Spring Boot(1.x版本)中集成Guava Cache 注意&#xff1a; Spring Boot 2.x用户&#xff1a;优先使用Caffeine&#xff0c;性能更优且维护活跃。 1. 添加依赖 在pom.xml中添加Guava依赖&#xff1a; <dependency><groupId>com.google.guava</groupId&…

Linux工作台文件操作命令全流程解析

全文目录 1 确认当前工作路径2 导航与目录管理2.1 关键命令2.2 逻辑衔接 3 文件基础操作3.1 创建 → 备份 → 重命名 → 清理3.2 文件查看和编辑3.3 文件链接3.4 文件diff 4 文件权限与所有权管理5 文件打包与归档6 参考文献 写在前面 shell是一种命令解释器&#xff0c;它提供…

LeetCode第183题_从不订购的客户

LeetCode 第183题&#xff1a;从不订购的客户 题目描述 表: Customers ---------------------- | Column Name | Type | ---------------------- | id | int | | name | varchar | ---------------------- id 是该表的主键。 该表包含消费者的 id 和…

c语言的常用关键字

c语言的常用关键字 c语言的关键字表示数据类型的关键字autocharfloatdoubleintlongshortvoidsignedstruct、enum、unionunsigned 表示分支语句的关键字ifelseswitchbreakcasecontinuedefault 表示循环语句的关键字whiledoforgoto 用于修饰变量或函数的关键字constconst修饰变量…

MCU通用输入输出端口(GPIO)设计指南

在嵌入式系统开发中&#xff0c;MCU的GPIO接口是一个基础但非常实用的功能模块。GPIO全称是通用输入输出端口&#xff0c;它让MCU可以灵活地与外部设备进行交互。 GPIO的主要特点包括&#xff1a; 多功能性&#xff1a;每个引脚都可以单独配置为输入或输出 可编程性&#xff…

STM32完整内存地址空间分配详解

在STM32这类基于ARM Cortex-M的32位微控制器中&#xff0c;整个4GB的地址空间(从0x00000000到0xFFFFFFFF)有着非常系统化的分配方案&#xff0c;每个区域都有其特定的用途。下面我将详细介绍这些地址区域的分配及其功能&#xff1a; STM32完整内存地址空间分配详解(0x00000000…

实现水平垂直居中的多种方法

在前端开发中&#xff0c;元素的居中是一个常见但又经常让人头疼的问题。本文将全面总结各种CSS居中方法&#xff0c;特别是如何实现一个div的水平垂直居中。 为什么居中这么重要&#xff1f; 居中布局是现代网页设计中最基础也最重要的布局方式之一。无论是导航菜单、登录框…

如何实现服务的自动扩缩容(Auto Scaling)

在云计算和分布式系统的时代,系统的弹性和适应性已成为企业构建高效IT基础设施的核心需求。自动扩缩容(Auto Scaling)作为一种关键技术,旨在根据实时负载变化动态调整计算资源,以确保系统性能稳定,同时优化资源利用效率。简单来说,自动扩缩容是指系统能够根据预设规则或…

uniapp+vue3+ts 使用canvas实现安卓端、ios端及微信小程序端二维码生成及下载

加粗样式uniapp多端生成带二维码海报并保存至相册的实现 在微信小程序开发中&#xff0c;我们常常会遇到生成带有二维码的海报并保存到手机相册的需求&#xff0c;比如分享活动海报、产品宣传海报等。今天就来和大家分享一下如何通过代码实现这一功能。 准备工作 在开始之前&am…

架构师面试(三十八):注册中心架构模式

题目 在微服务系统中&#xff0c;当服务达到一定数量时&#xff0c;通常需要引入【注册中心】组件&#xff0c;以方便服务发现。 大家有没有思考过&#xff0c;注册中心存在的最根本的原因是什么呢&#xff1f;注册中心在企业中的最佳实践是怎样的&#xff1f;注册中心的服务…

Day.js和Moment.js对比,日期时间库怎么选?

在JavaScript的日期处理库中&#xff0c;Moment.js 和 Day.js 是两个非常流行的选择。本文将基于从npmtrends的数据&#xff0c;对这两个库进行详细的对比分析。 Moment.js的重度使用者。凡是遇到时间和日期的操作&#xff0c;就把Moment.js引用上。 直到有天我发现加载的mome…

罗默如何用木星卫星“宇宙钟表”测量光速?

一、17世纪的“宇宙级实验” 1676年&#xff0c;丹麦天文学家奥勒罗默&#xff08;Ole Rmer&#xff09;在巴黎天文台做出惊人发现&#xff1a; 木星卫星的“迟到早退”现象&#xff0c;竟能揭示光速的秘密&#xff01; 通过观察木卫一&#xff08;Io&#xff09;的轨道周期变…