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

2021-07-30 16:06:45 浏览数 (1)

本篇就来看看如何把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定义动态库的调用函数。

调用动态库的顺序:

  1. 使用LoadLibrary来加载动态库。
  2. 使用GetProcAddress来加载动态库的调用函数。
  3. 调用上一步加载的函数。
  4. 释放动态库。

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目录下,所以会有这样提示,直接运行编译的程序是没有问题的。

0 人点赞