PaddleOCR C++动态库编译及调用识别(一)

学更好的别人,

做更好的自己。

——《微卡智享》

c36a0a3a944eb25bf993e848a5551c4b.png

本文长度为5106,预计阅读9分钟

前言

上一篇《飞桨PaddleOCR C++预测库布署》按照官方的流程做的,最后生成的为exe文件,真正调用时还需要将处理好的图片保存后本地再运行读取后才能识别,本篇就来看看如何把PaddleOCR的源码重新编译成动态库,供OpenCV的Demo调用。

5ff8ea4970b3f9302d212c2ecc4312e0.png

实现效果

b3472384e4eb070a1b42cfb057f54000.png

e7e7e24f0817bba27a9c6aa9c4d18136.png

7e217d4928378bcee5f015a86731b117.png

ddf7ac196380931cde21ba436c7148d5.png

d08c088e526eb8edc1f41f0a4438fa4d.png

e753261864dd5d1bb4428f802be60901.png

ed0ab89809b7270a70349e7472fe34e3.png

Q1

OCR识别效果怎么样?

做成动态库后,通过前一章提取的华容道图像,直接再进行OCR识别,说实话,自己感觉这个效果并不有达到我的预期。当然我觉得还是有优化的空间。

 

可优化的2点猜想:

1.因为本身想用模型较小的,所以采用的是PaddleOCR Lite的模型库,如果别大的效果应该会好。

2.通过预处理提取华容道棋盘,输出识别出的数字顺序没有细研究,所以感觉挺乱的。得不到想要的效果,下一步考虑再把每个格先预处理后单独识别看看。

总结

虽然说效果不是很尽人意,像第四张金色棋盘竟然一个数字也没识别出来,挺让我意外的,不过也是对自己有收获,像编译动态库再调用、关于C++输出中文乱码,过程中也花了些时间踩坑及填坑,这个半成品的代码也会在文章最后列出来,接下来正篇开始。

编译PaddleOCR动态库

c17c61068f684ff569a17a921ff95c80.png

微卡智享

01

修改ocr_rec.h和ocr_rec.cpp

a1450e3e669d70507f1f9e4aff59a875.png

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

363b34bb2ea91962b9812f3d2830b333.png

#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

79ba3593180f939db15a481bc411fdd0.png

#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

001842c9b14db291dffdab0f40fd35f5.png

在CMakeList里面修改挺简单的,因为原来的输出只有一个可执行的文件,这次我们需要动态库,所以加了三行,起的动态库名是PaddleOCRExport

set(BUILD_SHARED_LIBS ON)
add_library(PaddleOCRExport SHARED ${SRCS})
target_link_libraries(PaddleOCRExport ${DEPS})

做完上面三步,PaddleOCR的动态库就改完了,接下来就直接重新编译。

a4b9aae2df3657e7f4685bd491051a61.png

上图中可以看到,编译完后目录下面多出来了一个PaddleOCRExport.dll的动态库。

调用PaddleOCR动态库

764d4df7ec7e785ec8953b5ac5d7c493.png

微卡智享

01

整理输出的文件

1527e47eb2a405e8d5aa6dd53be27d55.png

我把们输出的配置文件都拷贝出来,要拷贝的东西《飞桨PaddleOCR C++预测库布署》这一篇中有详细讲解,把生成的orc_system.exe删了,这次不需要。

02

创建调用Demo

41c429b5a23656e6895c168a3118d0ae.png

创建一个OpenCVPaddleOCR的Demo,其中main里的代码和《C++ OpenCV检测并提取数字华容道棋盘》中是完全一样,直接复制过来的。

03

PaddleOCRApi调用类

接下来就是今天的核心内容了,创建一个PaddleOCR的动态库调用类。

30410aa12c6e4562289ab777f5a9381f.png

头文件中引入windows.h,然后使用typedef定义动态库的调用函数。

fc227efebbab15faca72802553b183dc.png

调用动态库的顺序:

  1. 使用LoadLibrary来加载动态库。

  2. 使用GetProcAddress来加载动态库的调用函数。

  3. 调用上一步加载的函数。

  4. 释放动态库。

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

调用函数

677bacbda9f3d49ef38e2f498d2aabbc.png

在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相关的所有文件,拷贝到刚才创建的动态库目录下。

2ac179dd379fff09b7c882112db4fd8a.png

然后运行程序即可以看到文章中开始的效果了。

0a62f85bdb2ffdaf4c29ca2d3e5a757c.png

遇到的问题

Q1

调用动态库Demo编译不过去?

最开始按原来的方法编译的Demo动态库,编译不成功,主要是引入了windows.h的库,使用using namespace cv这样的编译不过去。

 

解决这个问题,原来Demo中所有的using namespace都去掉了,然后每个函数前面都加上了命名空间,这块的就麻烦一点,不过编译也通过了。

 

Q2

OCR输出的中文乱码?

输出返回的OCR中文是乱码,这个是编码的问题。

 

解决这个在PaddleOCRApi的类里面加入了wstring和string的转换,因为本身返回的是string,所以需要先转为wstring再转回string,可以在上图中命令窗口输出的是中文。

但是有个问题,《C++ OpenCV输出中文》原来说过OpenCV的中文输出,这里我也把那个类加了进来,但是没有效果。

e505f4e38b87ee2c614c51806b9e4a3b.png

7651e2d2c07ced844c596ba7f715f5e8.png

Q1

拷贝过来的PaddleOCR动态库,调试运行不成功?

上面最后一步拷贝过来的所有相关PaddleOCR的文件,在Demo直接运行调试时不成功。

e4b685fa5dd4a4c32c1428588c465460.png

 

从上图中可以看出,提示是找不到config.txt的参数文件,动态库中里面的readOCRConfig函数读取的是动态库所在路径,

4f620b350dde1490361c424a5c707249.png

而我们拷贝到的目录是在Demo程序编译后的OpenCVPaddleOCR/x64/release目录下,所以会有这样提示,直接运行编译的程序是没有问题的。

 

源码地址

https://github.com/Vaccae/OpenCVDemoCpp.git

GitHub上不去的朋友,可以击下方的原文链接跳转到码云的地址,关注【微卡智享】公众号,回复【源码】可以下载我的所有开源项目。

4487caf0c5cc9943285b5beac08fc55f.png

扫描二维码

获取更多精彩

微卡智享

2dea6d1abc6eb00270dc870a59269460.png

「 往期文章 」

飞桨PaddleOCR C++预测库布署

C++ OpenCV检测并提取数字华容道棋盘

OpenCV旋转矩形RotatedRect的Points函数遇到的问题

 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vaccae

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值