说明: 本文编译的PaddleOCR版本:v2.8.1,关于windows下如何生成c++项目及如何编译PaddleOCR请参照我的上一篇文章《(二)Windows通过vs c++编译PaddleOCR-2.8.1-CSDN博客》,本文是上一个篇文章的延伸。
背景:
目前很多项目后端都是java EE编写,部署于linux和windows下,PaddleOCR通过hubServing很容易部署到linux下,但是因为使用python及一次http请求中转,速度很不理想,故而需要采用原始jni调用c++动态库的模式来部署ocr服务。
环境准备
参照《(二)Windows通过vs c++编译PaddleOCR-2.8.1-CSDN博客》生成vs 2022的c++项目。
书接上文,在d:\ppocr_cpp文件夹下新增一个jni文件夹存放jni头文件,再新增一个ocr_release文件夹存放编译后的javaclass及dll等文件,目录结构如下图:
一、JNI的编写
为了便于大家好测试及降低学习成本,我的java代码编写就使用文本文档编写。
我的本地java环境是jdk_1.8.0_411,因为JNI是很早就使用的成熟技术,所以目前现存的所有版本jdk都通过,大家使用自己顺手的jdk即可。
1、编写java jni代码
编写java jni功能,包含:加载动态库dll,初始化PPOCR,执行ocr识别。
在d:\PaddleOCR\ocr_release文件夹下新增PaddleOcrJni.java,注意文件编码是UTF-8,推荐使用vs code来编写,内容如下:
import java.io.File;
import java.io.IOException;
import java.util.Calendar;
/*
* @author : wuxiutong
* @date : 2024/10/14 14:38
* @description : PaddleOcrJni头文件
*/
public class PaddleOcrJni {
// 类加载的同时加载dll动态库,该代码也可以写入到具体方法中,只要调用前加载就可以,此处路径为了演示方便直接写死绝对路径,
static {
System.load("D:/ppocr_cpp/ocr_release/ppocr_jni.dll");
}
/***
* jni接口:传递文件完整路径,返回识别结果字符串。
* @param imgFullPath 图片文件完整路径
* @return 返回ocr识别后的json字符串大致格式如下{"status":"状态码","msg":"错误信息","results":[]} },status值000则是成功,否则失败
*/
public native String ocr(String[] imgFullPath);
/***
* jni接口:初始化ocr
* @param modelVer 模型版本,只v1、v2、v3、v4目前用最新的v4
* @param useGPU 是否使用GPU,1使用gpu,否则使用cpu
* @param useCls 是否使用方向识别类,1使用,否则不使用
* @param isTable 是否识别表格,1使用,否则不使用,返回表格结构
* @param isLayout 是否版面分析,1使用,否则不使用,通常和isTable配合使用
* @return 返回结果字符串{"status":"000","msg":"错误信息"},status值000则是成功,否则失败
*/
public native String init(int modelVer,int useGPU,int useCls,int isTable,int isLayout);
// main函数,此处用作具体演示,用到项目中则需要根据业务做具体调整。
public static void main(String[] args) {
try{
Calendar calendar = Calendar.getInstance();
// 执行初始化操作
PaddleOcrJni paddleOcrJni = new PaddleOcrJni();
// 执行PPOCR初始化
paddleOcrJni.init(4,0,1,0,0);
System.out.println("ocr初始化完成!");
// 将需要ocr识别的图片添加值待识别列表
String[] filesArray = new String[1];
filesArray[0] = new File("D:/ppocr_cpp/ocr_release/pic/jzpz.png").getCanonicalPath();
String ocrResult = paddleOcrJni.ocr(filesArray);
System.out.println("图片识别结果");
System.out.println(ocrResult);
System.out.println("耗时:" + (Calendar.getInstance().getTimeInMillis() - calendar.getTimeInMillis()) + "ms");
}catch(Exception e1){
System.out.println("执行失败,错误信息:"+e1.getMessage());
}
}
}
2、编译jni头文件
打开命令行,进入到d:\PaddleOCR\ocr_release文件夹下执行一下代码生成h头文件。
javac -encoding UTF-8 -h PaddleOcrJni.java
生成头文件,如下图所示。
3、添加头文件到vs项目中
将上一步生成的PaddleOcrJni.h文件复制到d:\PaddleOCR\jni文件夹下,再将jdk下的include文件夹中所有内容如下图所示的包含win32文件及所有h头文件,复制到到d:\PaddleOCR\jni下。
复制后的d:\PaddleOCR\jni文件夹内容如下图所示:
使用vs 2022 打开D:\ppocr_cpp\PaddleOCR\deploy\cpp_infer\build\ppocr.sln,如下图所示:
在打开vs 2022界面中右侧“解决方案资源管理器”中的的ppocr项目上右键,然后点击“属性”,在弹出的“ppocr属性页”弹窗中,左侧“配置属性”—> “C/C++” —> “常规” 中修改“附加包含目录”中点击“编辑”在弹出的“附件包含目录”弹窗中新增两行(千万不要删除已经存在的其他目录)分别指向“d:\ppocr_cpp\jni”和“d:\ppocr_cpp\jni\win32”,如以下图所示,最后确定并且应用。
以上操作的目的是让vs c++编译器能正确的检测到我们的java标准的头文件及上一步生成的PaddleOcrJni头文件,若后续新增c++编译时报错或者vs 2022别c++时如JNIEXPORT飘红等就需要检查此处是否设置正确。
二、修改vs项目为编译dll模式
在打开vs 2022的界面中右侧“解决方案资源管理器”中的的ppocr项目上右键,然后点击“属性”,在弹出的“ppocr属性页”弹窗中选择“配置属性”—>“常规”—>“配置类型”修改为动态库dll,文件名调整为ppocr_jni(可以任君调整,我这这是方便演示所有使用ppocr_jni),如下图所示:
在选择“配置属性”—>“高级”—>“配置类型”,将表格中的“目标文件扩展名”修改成.dll,如下图所示,最后应用即可。
三、编写c++中的jni实现
1、编写jni.cpp文件
在ppocr项目下的源代码文件夹中新增jni.cpp文件。
以下是jni.cpp文件的全部内容:
#include "opencv2/core.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
#include <vector>
#include <time.h>
#include <include/args.h>
#include <include/paddleocr.h>
#include <include/paddlestructure.h>
#include <PaddleOcrJni.h>
#include <fstream>
#include <iostream>
#include <string>
#include <exception>
using namespace PaddleOCR;
PPOCR ocr;
// modelVer v1\v2\v3\v4
JNIEXPORT jstring JNICALL Java_PaddleOcrJni_init
(JNIEnv* env, jobject, jint modelVer, jint useGPU, jint useCls, jint isTable, jint isLayout) {
try {
FLAGS_det_model_dir = "./infer/ch_PP-OCRv4_det_infer";
FLAGS_rec_model_dir = "./infer/ch_PP-OCRv4_rec_infer";
FLAGS_cls_model_dir = "./infer/ch_ppocr_mobile_v2.0_cls_infer";
FLAGS_rec_char_dict_path = "./utils/ppocr_keys_v1.txt";
FLAGS_layout_dict_path = "./utils/dict/layout_dict/layout_publaynet_dict.txt";
FLAGS_table_char_dict_path = "./utils/dict/table_structure_dict_ch.txt";
if (useCls == 1) {
FLAGS_cls = true;
}
// 执行初始化操作
ocr.init();
std::ostringstream rstOSS;
rstOSS << "{\"msg\":\"\",\"status\" : \"000\"}";
std::string ocrRstStr = rstOSS.str();
jstring rr = env->NewStringUTF(ocrRstStr.c_str());
return rr;
}
catch (const std::exception& e) {
// 捕获所有标准类异常
std::ostringstream rstOSS;
rstOSS << "{\"msg\":\"ocr初始化失败,失败详情:";
rstOSS << e.what();
rstOSS << "\",\"status\" : \"001\"}";
std::string ocrRstStr = rstOSS.str();
jstring rr = env->NewStringUTF(ocrRstStr.c_str());
return rr;
}
catch (...) {
// 捕获所有非标准类异常(自定义)
std::ostringstream rstOSS;
rstOSS << "{\"msg\":\"ocr初始化失败,未知异常\",\"status\" : \"001\"}";
std::string ocrRstStr = rstOSS.str();
jstring rr = env->NewStringUTF(ocrRstStr.c_str());
return rr;
}
}
/**
jni传入
*/
JNIEXPORT jstring JNICALL Java_PaddleOcrJni_ocr(JNIEnv* env, jobject, jobjectArray stringArray) {
// 获取数组长度
jsize length = env->GetArrayLength(stringArray);
std::cerr << "------recived image path count: " << length << "------" << std::endl;
std::vector<cv::Mat> img_list;
// 创建一个C字符串数组来存储转换后的字符串
std::vector<char*> cStrings(length);
try {
for (jsize i = 0; i < length; ++i) {
// 获取Java字符串对象
jstring javaString = (jstring)env->GetObjectArrayElement(stringArray, i);
// 检查是否获取到有效的Java字符串对象
if (javaString == nullptr) {
throw std::runtime_error("------Failed to get Java string element------");
}
// 将Java字符串转换为C字符串
const char* cString = env->GetStringUTFChars(javaString, nullptr);
// 存储C字符串的指针
cStrings[i] = strdup(cString); // 注意:strdup分配了内存,需要后续释放
// 释放JNI分配的字符串内存
env->ReleaseStringUTFChars(javaString, cString);
// 释放Java字符串对象的本地引用(避免本地引用泄漏)
env->DeleteLocalRef(javaString);
}
// 在这里处理cStrings数组中的C字符串
for (jsize i = 0; i < length; ++i) {
cv::Mat mat = cv::imread(cStrings[i], cv::IMREAD_COLOR);
img_list.push_back(mat);
}
// 释放C字符串数组中的内存
for (jsize i = 0; i < length; ++i) {
free(cStrings[i]);
}
}
catch (const std::exception& e) {
// 处理异常,例如打印错误信息
std::cerr << "------Error processing string array: " << e.what() << "------" << std::endl;
// 释放已分配的C字符串内存(如果有的话)
for (jsize i = 0; i < length; ++i) {
if (cStrings[i] != nullptr) {
free(cStrings[i]);
}
}
}
try {
if (img_list.size() <= 0) {
std::ostringstream rstOSS;
rstOSS << "{\"msg\":\"未提供文件列表\", \"results\":[],\"status\" : \"001\"}";
std::string ocrRstStr = rstOSS.str();
jstring rr = env->NewStringUTF(ocrRstStr.c_str());
return rr;
}
std::cerr << "------ocr images count: " << img_list.size() << "------" << std::endl;
std::vector<std::vector<OCRPredictResult>> ocr_results = ocr.ocr(img_list, true, true, false);
// 将识别对象转换为json字符串
std::string ocrRstStr = "";
//{"msg":"","results":[[],[],[]],"status":"000"}
std::ostringstream resultsOSS;
for (int rstCount = 0;rstCount < ocr_results.size();rstCount++) {
for (int i = 0;i < ocr_results[rstCount].size();i++) {
std::ostringstream singleRstOss;
OCRPredictResult rst = ocr_results[rstCount][i];
singleRstOss << "{";
// 预测可信任值
singleRstOss << "\"confidence\":";
singleRstOss << rst.score;
// 识别字符串
singleRstOss << ",";
singleRstOss << "\"text\":";
singleRstOss << ("\"" + rst.text + "\"");
singleRstOss << (",");
// 定位区域
singleRstOss << ("\"text_region\":[");
std::string boxStr = "";
std::vector<std::vector<int>> boxes = rst.box;
for (int n = 0; n < boxes.size(); n++) {
singleRstOss << ("[");
singleRstOss << boxes[n][0];
singleRstOss << ",";
singleRstOss << boxes[n][1];
singleRstOss << ("]");
if (n != rst.box.size() - 1) {
singleRstOss << (",");
}
}
singleRstOss << (boxStr.c_str());
singleRstOss << ("]");
singleRstOss << ("}");
if (i != 0) {
resultsOSS << ",";
}
resultsOSS << "[";
resultsOSS << singleRstOss.str();
resultsOSS << "]";
}
}
// 拼接最后结果
ocrRstStr = "{\"msg\":\"\",\"results\":[" + resultsOSS.str() + "],\"status\":\"000\"}";
jstring rr = env->NewStringUTF(ocrRstStr.c_str());
return rr;
}
catch (std::exception& e) {
std::cout << "------ocr exception:" << e.what() << ",detail:" << std::endl;
std::string err = "";
std::ostringstream singleRstOss;
singleRstOss << e.what();
err.append("{\"msg\":\"");
err.append(singleRstOss.str());
err.append("\",\"results\":[],\"status\":\"err\"}");
jstring rr = env->NewStringUTF(err.c_str());
return rr;
}
}
2、修改paddleocr.cpp文件
修改ppocr项目下的“外部依赖项”paddleocr.h文件中新增一个成员函数init()成员函数,添加后的结果如下图:
修改ppocr项目下的“源文件”下的paddleocrc.cpp文件,新增init()方法,将原PPOCR()构造函数中的内容添加init()方法中,清空PPOCR()构造函数中的内容,调整后的代码如下图:
四、编译ppocr
1、编译ppocr_jni.dll文件
完成代码编写后就可以点击“生成”—>“重新生成解决方案”,如果系统不报错即可在D:\ppocr_cpp\PaddleOCR\deploy\cpp_infer\build\Release下看到已经生成ppcor_jni.dll文件,结果如下所示:
2、复制生成的ppocr_jni.dll等文件
复制上一步生成的Release文件夹下的所有dll文件到D:\ppocr_cpp\ocr_release文件夹中,复制完成后如下图所示:
3、复制D:\ppocr_cpp\PaddleOCR\ppocr\utils文件夹
将D:\ppocr_cpp\PaddleOCR\ppocr下的utils文件夹整个复制到D:\ppocr_cpp\ocr_release文件夹中,复制后D:\ppocr_cpp\ocr_release\utils\文件夹下将包含ocr需要使用的到ppocr_keys_v1.txt字典等文件,结果如下图所示:
4、复制D:\ppocr_cpp\infer文件夹
将D:\ppocr_cpp\infer文件夹整个复制到D:\ppocr_cpp\ocr_release下,包含了ocr需要使用到的3个模型,复制后的截图如下图:
五、运行查看效果
运行前检查d:\ppocr_cpp\ocr_release目录文件信息是否完整,完整文件如下:
打开命令行,进入到d:\ppocr_cpp\ocr_release目录下,运行以下脚本:
java PaddleOcrJni
如下图:
效果:
因为java代码中指定了待识别ocr图片在D:/ppocr_cpp/ocr_release/pic/jzpz.png,所以识别结果返回err。
添加一张有文字的png图片到D:/ppocr_cpp/ocr_release/pic下并命名为jzpz.png再次执行代码即可查看到最终效果:
至此将PaddleOCR编译成dll动态库,供java调用实现java的ocr识别就到结束,以上代码为本人编写的demo,具体的可在此基础上按业务需求做改造。
后话:
上面代码中可以发现我jni接口中有一个单独的init方法,并且修改了paddleocr.h和paddleocr.cpp中的默认构造函数,我的目的有二:
1、解决官方代码样例(main.cpp)中的PPOCR每次初始化对象的时候就加载ocr模型及字典等信息(args.cpp文件中定义,ppocr.exe运行时给参),我需要的是固化和动态通过jni传递这些参数,但是我又不愿意去修改args.cpp文件,因为说不定官方在后续某个版本中增加参数会修改该文件,所以我调整了官方的paddleocr的构造函数,新增一个init函数来由我jni调用它来初始化PPOCR,尽量降低对官方代码的修改,方便后续升级。
2、解决java每次调用ocr动态库做ocr识别时都会重新初始化PPOCR(初始化会耗时),所有我的java代码在使用时会修改成单例模式,只有第一次识别ocr的时候完成PPOCR的初始化,后续的ocr识别就不用再初始化,从而提高调用OCR所耗费的时间。
后续文章应该会写关于PaddleOCR识别的模型训练教程,因为目前这个版本(v4)的模型库有些汉字是无法识别的,如蒯字(大家可以试试),所以实际使用中需要自己训练模型。