本篇就来看看如何把PaddleOCR的源码重新编译成动态库,供OpenCV的Demo调用。
实现效果
Q1
OCR识别效果怎么样?
做成动态库后,通过前一章提取的华容道图像,直接再进行OCR识别,说实话,自己感觉这个效果并不有达到我的预期。当然我觉得还是有优化的空间。
可优化的2点猜想:
1.因为本身想用模型较小的,所以采用的是PaddleOCR Lite的模型库,如果别大的效果应该会好。
2.通过预处理提取华容道棋盘,输出识别出的数字顺序没有细研究,所以感觉挺乱的。得不到想要的效果,下一步考虑再把每个格先预处理后单独识别看看。
总结
虽然说效果不是很尽人意,像第四张金色棋盘竟然一个数字也没识别出来,挺让我意外的,不过也是对自己有收获,像编译动态库再调用、关于C 输出中文乱码,过程中也花了些时间踩坑及填坑,这个半成品的代码也会在文章最后列出来,接下来正篇开始。
编译PaddleOCR动态库
微卡智享
01
修改ocr_rec.h和ocr_rec.cpp
ocr_rec这个类主要就是OCR的识别类,原来的Run函数直接就输出识别的中文了,并没有返回任何文本,所以这里我们要自己增加一个处理的函数。
在上图Run函数下面增加了一个RunOCR的函数,返回vector<string>的识别容器。实现方法和Run基本一致,直接贴出ocr_rec.cpp中的函数
代码语言:javascript复制std::vector<std::string> CRNNRecognizer::RunOCR(std::vector<std::vector<std::vector<int>>> boxes, cv::Mat& img, Classifier* cls)
{
cv::Mat srcimg;
img.copyTo(srcimg);
cv::Mat crop_img;
cv::Mat resize_img;
std::cout << "The predicted text is :" << std::endl;
int index = 0;
std::vector<std::string> str_res;
for (int i = 0; i < boxes.size(); i ) {
crop_img = GetRotateCropImage(srcimg, boxes[i]);
if (cls != nullptr) {
crop_img = cls->Run(crop_img);
}
float wh_ratio = float(crop_img.cols) / float(crop_img.rows);
this->resize_op_.Run(crop_img, resize_img, wh_ratio, this->use_tensorrt_);
this->normalize_op_.Run(&resize_img, this->mean_, this->scale_,
this->is_scale_);
std::vector<float> input(1 * 3 * resize_img.rows * resize_img.cols, 0.0f);
this->permute_op_.Run(&resize_img, input.data());
// Inference.
auto input_names = this->predictor_->GetInputNames();
auto input_t = this->predictor_->GetInputHandle(input_names[0]);
input_t->Reshape({ 1, 3, resize_img.rows, resize_img.cols });
input_t->CopyFromCpu(input.data());
this->predictor_->Run();
std::vector<float> predict_batch;
auto output_names = this->predictor_->GetOutputNames();
auto output_t = this->predictor_->GetOutputHandle(output_names[0]);
auto predict_shape = output_t->shape();
int out_num = std::accumulate(predict_shape.begin(), predict_shape.end(), 1,
std::multiplies<int>());
predict_batch.resize(out_num);
output_t->CopyToCpu(predict_batch.data());
// ctc decode
int argmax_idx;
int last_index = 0;
float score = 0.f;
int count = 0;
float max_value = 0.0f;
for (int n = 0; n < predict_shape[1]; n ) {
argmax_idx =
int(Utility::argmax(&predict_batch[n * predict_shape[2]],
&predict_batch[(n 1) * predict_shape[2]]));
max_value =
float(*std::max_element(&predict_batch[n * predict_shape[2]],
&predict_batch[(n 1) * predict_shape[2]]));
if (argmax_idx > 0 && (!(n > 0 && argmax_idx == last_index))) {
score = max_value;
count = 1;
str_res.push_back(label_list_[argmax_idx]);
}
last_index = argmax_idx;
}
score /= count;
for (int i = 0; i < str_res.size(); i ) {
std::cout << str_res[i];
}
std::cout << "tscore: " << score << std::endl;
}
return str_res;
}
02
创建外部调用的头文件和源文件
本身PaddleOCR的源码相关比较多,所以这里我只贴出来我自己修改的部分,可以直接从文中复制,最后的Demo里面只有编译好的动态库和调用的源码。
ocr_export.h
代码语言:javascript复制#pragma once
#include <iostream>
#include <direct.h>
#include <stdio.h>
#include <codecvt>
#include "opencv2/core.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <include/config.h>
#include <include/ocr_det.h>
#include <include/ocr_rec.h>
#define DLLEXPORT __declspec(dllexport)
#ifdef __cplusplus
extern "C" {
#endif
DLLEXPORT char* PaddleOCRText(cv::Mat& img);
#ifdef __cplusplus
}
#endif
PaddleOCR::OCRConfig readOCRConfig();
其中PaddleOCRText为动态库外部调用的函数,readOCRConfig是读取参数的函数。
ocr_export.cpp
代码语言:javascript复制#include <include/ocr_export.h>
DLLEXPORT char* PaddleOCRText(cv::Mat& img)
{
std::vector<std::string> str_res;
std::string tmpstr;
if (!img.data) {
return "could not read Mat ";
}
PaddleOCR::OCRConfig config = readOCRConfig();
//打印config参数
config.PrintConfigInfo();
//图像检测文本
PaddleOCR::DBDetector det(config.det_model_dir, config.use_gpu, config.gpu_id,
config.gpu_mem, config.cpu_math_library_num_threads,
config.use_mkldnn, config.max_side_len, config.det_db_thresh,
config.det_db_box_thresh, config.det_db_unclip_ratio,
config.use_polygon_score, config.visualize,
config.use_tensorrt, config.use_fp16);
PaddleOCR::Classifier* cls = nullptr;
if (config.use_angle_cls == true) {
cls = new PaddleOCR::Classifier(config.cls_model_dir, config.use_gpu, config.gpu_id,
config.gpu_mem, config.cpu_math_library_num_threads,
config.use_mkldnn, config.cls_thresh,
config.use_tensorrt, config.use_fp16);
}
PaddleOCR::CRNNRecognizer rec(config.rec_model_dir, config.use_gpu, config.gpu_id,
config.gpu_mem, config.cpu_math_library_num_threads,
config.use_mkldnn, config.char_list_file,
config.use_tensorrt, config.use_fp16);
//检测文本框
std::vector<std::vector<std::vector<int>>> boxes;
det.Run(img, boxes);
//OCR识别
str_res = rec.RunOCR(boxes, img, cls);
std::wstring_convert<std::codecvt_utf8<wchar_t>> conv;
for (auto item : str_res) {
tmpstr = item;
}
char* reschar = new char[tmpstr.length() 1];
tmpstr.copy(reschar, std::string::npos);
return reschar;
}
PaddleOCR::OCRConfig readOCRConfig()
{
////保证config.txt从本DLL目录位置读取
//获取DLL自身所在路径(此处包括DLL文件名)
char DllPath[_MAX_PATH] = { 0 };
getcwd(DllPath, _MAX_PATH);
std::cout << DllPath << std::endl;
strcat(DllPath, "\config.txt");
std::cout << DllPath << std::endl;
////截取DLL所在目录(去掉DLL文件名)
//char drive[_MAX_DRIVE];
//char dir[_MAX_DIR];
//char fname[_MAX_FNAME];
//char ext[_MAX_EXT];
//_splitpath(DllPath, drive, dir, fname, ext);
////字符串拼接
//strcat(dir, "config.txt");
return PaddleOCR::OCRConfig(DllPath);
}
注:参数中返回用的char*也是自己测试了挺久,用过返回string,或是传入vector<string>的指针都有问题,主要是C 的基础还不够,当然这个踩坑和填坑的过程中成长倒是挺多的。
03
修改CMakeList
在CMakeList里面修改挺简单的,因为原来的输出只有一个可执行的文件,这次我们需要动态库,所以加了三行,起的动态库名是PaddleOCRExport
代码语言:javascript复制set(BUILD_SHARED_LIBS ON)
add_library(PaddleOCRExport SHARED ${SRCS})
target_link_libraries(PaddleOCRExport ${DEPS})
做完上面三步,PaddleOCR的动态库就改完了,接下来就直接重新编译。
上图中可以看到,编译完后目录下面多出来了一个PaddleOCRExport.dll的动态库。
调用PaddleOCR动态库
微卡智享
01
整理输出的文件
我把们输出的配置文件都拷贝出来,要拷贝的东西《飞桨PaddleOCR C 预测库布署》这一篇中有详细讲解,把生成的orc_system.exe删了,这次不需要。
02
创建调用Demo
创建一个OpenCVPaddleOCR的Demo,其中main里的代码和《C OpenCV检测并提取数字华容道棋盘》中是完全一样,直接复制过来的。
03
PaddleOCRApi调用类
接下来就是今天的核心内容了,创建一个PaddleOCR的动态库调用类。
头文件中引入windows.h,然后使用typedef定义动态库的调用函数。
调用动态库的顺序:
- 使用LoadLibrary来加载动态库。
- 使用GetProcAddress来加载动态库的调用函数。
- 调用上一步加载的函数。
- 释放动态库。
PaddleOcrApi.h
代码语言:javascript复制#pragma once
//通过调用windowsAPI 来加载和卸载DLL
#include <Windows.h>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <locale>
#include <codecvt>
class PaddleOcrApi
{
private:
typedef char*(*DllFun)(cv::Mat&);
public:
static std::string GetPaddleOCRText(cv::Mat& src);
// string的编码方式为utf8,则采用:
static std::string wstr2utf8str(const std::wstring& str);
static std::wstring utf8str2wstr(const std::string& str);
// string的编码方式为除utf8外的其它编码方式,可采用:
static std::string wstr2str(const std::wstring& str, const std::string& locale);
static std::wstring str2wstr(const std::string& str, const std::string& locale);
};
PaddleOcrApi.cpp
代码语言:javascript复制#include "PaddleOcrApi.h"
std::string PaddleOcrApi::GetPaddleOCRText(cv::Mat& src)
{
std::string resstr;
DllFun funName;
HINSTANCE hdll;
try
{
hdll = LoadLibrary(L"PaddleOCRExport.dll");
if (hdll == NULL)
{
resstr = "加载不到PaddleOCRExport.dll动态库!";
FreeLibrary(hdll);
return resstr;
}
funName = (DllFun)GetProcAddress(hdll, "PaddleOCRText");
if (funName == NULL)
{
resstr = "找不到PaddleOCRText函数!";
FreeLibrary(hdll);
return resstr;
}
resstr = funName(src);
// 将utf-8的string转换为wstring
std::wstring wtxt = utf8str2wstr(resstr);
// 再将wstring转换为gbk的string
resstr = wstr2str(wtxt, "Chinese");
FreeLibrary(hdll);
}
catch (const std::exception& ex)
{
resstr = ex.what();
return "Error:" resstr;
FreeLibrary(hdll);
}
return resstr;
}
std::string PaddleOcrApi::wstr2utf8str(const std::wstring& str)
{
static std::wstring_convert<std::codecvt_utf8<wchar_t> > strCnv;
return strCnv.to_bytes(str);
}
std::wstring PaddleOcrApi::utf8str2wstr(const std::string& str)
{
static std::wstring_convert< std::codecvt_utf8<wchar_t> > strCnv;
return strCnv.from_bytes(str);
}
std::string PaddleOcrApi::wstr2str(const std::wstring& str, const std::string& locale)
{
typedef std::codecvt_byname<wchar_t, char, std::mbstate_t> F;
static std::wstring_convert<F> strCnv(new F(locale));
return strCnv.to_bytes(str);
}
std::wstring PaddleOcrApi::str2wstr(const std::string& str, const std::string& locale)
{
typedef std::codecvt_byname<wchar_t, char, std::mbstate_t> F;
static std::wstring_convert<F> strCnv(new F(locale));
return strCnv.from_bytes(str);
}
04
调用函数
在main.cpp中每张截取棋盘后的Mat后加入调用PaddleOCR的识别,然后再putText显示出来。
代码语言:javascript复制 //载取透视变换后的图像显示出来
cv::Rect cutrect = cv::Rect(rectPoint[0], rectPoint[2]);
cv::Mat cutMat = resultimg(cutrect);
//使用PaddleOCR识别
std::string resstr = PaddleOcrApi::GetPaddleOCRText(cutMat);
std::cout << "OCR:" << resstr << std::endl;
//输出识别文字
putText::putTextZH(cutMat, resstr.data(), cv::Point(20, 20), cv::Scalar(0, 0, 255), 1);
cv::putText(cutMat, resstr, cv::Point(20, 50), 1, 1, cv::Scalar(0, 0, 255));
CvUtils::SetShowWindow(cutMat, "cutMat", 600, 20);
cv::imshow("cutMat", cutMat);
05
将PaddleOCR动态库拷贝到Demo目录下
第一步我们编译并整理好的PaddleOCR相关的所有文件,拷贝到刚才创建的动态库目录下。
然后运行程序即可以看到文章中开始的效果了。
遇到的问题
Q1
调用动态库Demo编译不过去?
最开始按原来的方法编译的Demo动态库,编译不成功,主要是引入了windows.h的库,使用using namespace cv这样的编译不过去。
解决这个问题,原来Demo中所有的using namespace都去掉了,然后每个函数前面都加上了命名空间,这块的就麻烦一点,不过编译也通过了。
Q2
OCR输出的中文乱码?
输出返回的OCR中文是乱码,这个是编码的问题。
解决这个在PaddleOCRApi的类里面加入了wstring和string的转换,因为本身返回的是string,所以需要先转为wstring再转回string,可以在上图中命令窗口输出的是中文。
但是有个问题,《C OpenCV输出中文》原来说过OpenCV的中文输出,这里我也把那个类加了进来,但是没有效果。
Q1
拷贝过来的PaddleOCR动态库,调试运行不成功?
上面最后一步拷贝过来的所有相关PaddleOCR的文件,在Demo直接运行调试时不成功。
从上图中可以看出,提示是找不到config.txt的参数文件,动态库中里面的readOCRConfig函数读取的是动态库所在路径,
而我们拷贝到的目录是在Demo程序编译后的OpenCVPaddleOCR/x64/release目录下,所以会有这样提示,直接运行编译的程序是没有问题的。