学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为5106字,预计阅读9分钟
前言
上一篇《飞桨PaddleOCR C++预测库布署》按照官方的流程做的,最后生成的为exe文件,真正调用时还需要将处理好的图片保存后本地再运行读取后才能识别,本篇就来看看如何把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中的函数
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
#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
#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
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
#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
#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显示出来。
//载取透视变换后的图像显示出来
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目录下,所以会有这样提示,直接运行编译的程序是没有问题的。
源码地址
https://github.com/Vaccae/OpenCVDemoCpp.git
GitHub上不去的朋友,可以击下方的原文链接跳转到码云的地址,关注【微卡智享】公众号,回复【源码】可以下载我的所有开源项目。
完
扫描二维码
获取更多精彩
微卡智享
「 往期文章 」