PaddleOCR C++学习笔记(二)

学更好的别人,

做更好的自己。

——《微卡智享》

44a3d0f32116cd6cd3c83227cdb074c6.png

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

前言

上一篇《PaddleOCR C++动态库编译及调用识别(一)》中把PaddleOCR的动态库编译完也调用成功,也考虑了几个可以优化的方法,本来也是想按自己的想法做的优化,过程中也踩到了不少的坑,慢慢填吧。这篇文章算是做了一个踩坑的记录。

7d5ca5f31fb7a65d218c8b88d98ff789.png

上篇提的优化方向

d5419e76c51b0a6967f6407cc221ae29.png

上图中可以看到,上一篇说过的两个优化方向:

  1. 替换通用的OCR识别模型

  2. 分割华容道图片,单张识别

替换通用的OCR模型

01

下载通用OCR模型

这个比较简单,直接在PaddleOCR的源码里面找到对应的推理模型下载替换。

f0ed078e01f152d1b16827c10ff0a0c3.png

其中中间那个方向分类器的模型和轻量的模型是一样的,所以这个可以不用下载。

8e9176aff3e5b3f7613342c476e86904.png

下载完解压后我们一样拷贝到定义的模型目录下,可以看到只替换det_infer的检测模型和rec_infer的识别模型,中间带有server的就是新下载的通用OCR模型。

02

修改Config.txt配置文件

2795f6543a5b1803f3ed0fe6921da087.png

接下来返回上级目录修改Config.txt的配置文件,将检测模型det_model_dir和识别模型rec_model_dir修改为刚刚下载的通用OCR模型的路径即可。

对比效果

74107c64f23613474ac30349245b7ee5.png

通用OCR模型

7a8a192f4c295ff9d6348fbfff0292b0.png

轻量OCR模型

替换了确实识别率比原来的模型好的,从上图中可以看到,原来数字华容道识别为数字革容道,而通用模型识别就完全没有问题。

问题一解决。

227f642f6f6a8a66abb82eaf613a7702.png

分割数字华容道棋盘

麻烦的终于来,这个真是花了不少时间,过程中由于没有截图,这里就说说问题吧。

1.通过检测矩形进行区分

最先考虑的是检测透视变换后的图像,从中轮廓查找所有的矩形,但是由于图片光照或其它原因,通过二值化或是边缘检测,都会存在未闭合的情况,没法做区分,效果并不好,所以放弃。

2.通过距离变换与分水岭分割

  1. 用拉普拉斯算子提高图像对比度

  2. 二值化图像后进行距离变换

  3. 对距离变换后的再进行归一化

  4. 查找轮廓并实现分水岭分割
    用上面的方法输出的效果也并不是想要的,所以这个也放弃了。

3.霍夫直线检测

上面的两个效果不好,然后就想已经通过透视变换将图像矫正过来了,所以用直线检测后计算点来定位矩形再分割。使用中霍夫直接的函数调参花了不少时间,效果也不好,并且后续的处理应该也很麻烦,所以暂时也放弃了。

4.透视变换迭代

上面三个方法是在图像中查找16个矩形再分割,处理的效果都不理想,所以就考虑透视变换的图像中再做处理,在原来的透视变换中再做迭代的处理,再查找一次里面的最大正方形截取出来,这样直接进行等比分割就简单多了,于是就把原来main.cpp里面预处理透视变换的代码封装了函数,用递归的方式实现自定义迭代次数。

c3611caaaee765d8d4d30f7c36ee770d.png

cv::Mat PaddleOcrApi::GetPerspectiveMat(cv::Mat& src, int iterations)
{
  cv::Mat tmpsrc, cannysrc, resultMat;
  src.copyTo(tmpsrc);


  //高斯滤波
  cv::GaussianBlur(tmpsrc, tmpsrc, cv::Size(5, 5), 0.5, 0.5);


  int srcArea = tmpsrc.size().area();
  float maxArea = 0;
  int maxAreaidx = -1;


  std::vector<cv::Mat> channels;
  cv::Mat B_src, G_src, R_src, dstmat;
  cv::split(tmpsrc, channels);


  int minthreshold = 120, maxthreshold = 200;


  //B进行Canny
  //大津法求阈值
  CvUtils::GetMatMinMaxThreshold(channels[0], minthreshold, maxthreshold, 1);
  std::cout << "OTSUmin:" << minthreshold << "  OTSUmax:" << maxthreshold << std::endl;
  //Canny边缘提取
  cv::Canny(channels[0], B_src, minthreshold, maxthreshold);


  //大津法求阈值
  CvUtils::GetMatMinMaxThreshold(channels[1], minthreshold, maxthreshold, 1);
  std::cout << "OTSUmin:" << minthreshold << "  OTSUmax:" << maxthreshold << std::endl;
  //Canny边缘提取
  Canny(channels[1], G_src, minthreshold, maxthreshold);


  //大津法求阈值
  CvUtils::GetMatMinMaxThreshold(channels[2], minthreshold, maxthreshold, 1);
  std::cout << "OTSUmin:" << minthreshold << "  OTSUmax:" << maxthreshold << std::endl;
  //Canny边缘提取
  Canny(channels[2], R_src, minthreshold, maxthreshold);




  bitwise_or(B_src, G_src, dstmat);
  bitwise_or(R_src, dstmat, dstmat);
  //CvUtils::SetShowWindow(dstmat, "dstmat", 700, 20);
  //imshow("dstmat", dstmat);




  std::vector<std::vector<cv::Point>> contours;
  std::vector<cv::Vec4i> hierarchy;
  findContours(dstmat, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);


  cv::Mat dstcontour = cv::Mat::zeros(cannysrc.size(), CV_8SC3);
  cv::Mat tmpcontour;
  dstcontour.copyTo(tmpcontour);


  //定义拟合后的多边形数组
  std::vector<std::vector<cv::Point>> vtshulls(contours.size());


  for (int i = 0; i < contours.size(); ++i) {
    //判断轮廓形状,不是四边形的忽略掉
    double lensval = 0.01 * arcLength(contours[i], true);
    std::vector<cv::Point> convexhull;
    approxPolyDP(cv::Mat(contours[i]), convexhull, lensval, true);


    //拟合的多边形存放到定义的数组中
    vtshulls[i] = convexhull;


    //不是四边形的过滤掉
    if (convexhull.size() != 4) continue;


    //求出最小旋转矩形
    cv::RotatedRect rRect = minAreaRect(contours[i]);
    //更新最小旋转矩形中面积最大的值
    if (rRect.size.height == 0) continue;


    if (rRect.size.area() > maxArea && rRect.size.area() > srcArea * 0.1
      && !CvUtils::CheckRectBorder(src, rRect)) {
      maxArea = rRect.size.area();
      maxAreaidx = i;
    }
  }


  //找到符合条码的最大面积的轮廓进行处理
  if (maxAreaidx >= 0) {
    std::cout << "iterations:" << iterations << "  maxAreaidx:" << maxAreaidx << std::endl;
    //获取最小旋转矩形
    cv::RotatedRect rRect = minAreaRect(contours[maxAreaidx]);
    cv::Point2f vertices[4];
    //重新排序矩形坐标点,按左上,右上,右下,左下顺序
    CvUtils::SortRotatedRectPoints(vertices, rRect);


    std::cout << "Rect:" << vertices[0] << vertices[1] << vertices[2] << vertices[3] << std::endl;


    //根据获得的4个点画线
    for (int k = 0; k < 4; ++k) {
      line(dstcontour, vertices[k], vertices[(k + 1) % 4], cv::Scalar(255, 0, 0));
    }


    //计算四边形的四点坐标
    cv::Point2f rPoints[4];
    CvUtils::GetPointsFromRect(rPoints, vertices, vtshulls[maxAreaidx]);
    for (int k = 0; k < 4; ++k) {
      line(dstcontour, rPoints[k], rPoints[(k + 1) % 4], cv::Scalar(255, 255, 255));
    }




    //采用离最小矩形四个点最近的重新设置范围,将所在区域的点做直线拟合再看看结果
    cv::Point2f newPoints[4];
    CvUtils::GetPointsFromFitline(newPoints, rPoints, vertices);
    for (int k = 0; k < 4; ++k) {
      line(dstcontour, newPoints[k], newPoints[(k + 1) % 4], cv::Scalar(255, 100, 255));
    }




    //根据最小矩形和多边形拟合的最大四个点计算透视变换矩阵    
    cv::Point2f rectPoint[4];
    //计算旋转矩形的宽和高
    float rWidth = CvUtils::CalcPointDistance(vertices[0], vertices[1]);
    float rHeight = CvUtils::CalcPointDistance(vertices[1], vertices[2]);
    //计算透视变换的左上角起始点
    float left = dstcontour.cols;
    float top = dstcontour.rows;
    for (int i = 0; i < 4; ++i) {
      if (left > newPoints[i].x) left = newPoints[i].x;
      if (top > newPoints[i].y) top = newPoints[i].y;
    }


    rectPoint[0] = cv::Point2f(left, top);
    rectPoint[1] = rectPoint[0] + cv::Point2f(rWidth, 0);
    rectPoint[2] = rectPoint[1] + cv::Point2f(0, rHeight);
    rectPoint[3] = rectPoint[0] + cv::Point2f(0, rHeight);




    //计算透视变换矩阵    
    cv::Mat warpmatrix = getPerspectiveTransform(rPoints, rectPoint);
    cv::Mat resultimg;
    //透视变换
    warpPerspective(src, resultimg, warpmatrix, resultimg.size(), cv::INTER_LINEAR);


    /*CvUtils::SetShowWindow(resultimg, "resultimg", 200, 20);
    imshow("resultimg", resultimg);*/


    //载取透视变换后的图像显示出来
    cv::Rect cutrect = cv::Rect(rectPoint[0], rectPoint[2]);
    resultMat = resultimg(cutrect);


    //CvUtils::SetShowWindow(resultMat, "resultMat", 600, 20);
    //cv::imshow("resultMat", resultMat);


    iterations--;
    if (iterations > 0) {
      resultMat = GetPerspectiveMat(resultMat, iterations);
    }
  }
  else {
    src.copyTo(resultMat);
  }
  return resultMat;
}


c975c0f3bfcdddb66acf8b283aba4d1b.png

调用时把参数改为2,做两次透视变换。

对比效果

4807bc44f2a7f9c9d7a456f6fb6c8e91.png

fd2af556450e2e537e1c789c53d02387.png

这张效果是一样的

94aa59cd2b3ef1e1a244e7b1d7159240.png

7429a2d8a3be9f1cb30965f723db269e.png

c08913b1a18c0355190758dea416b912.png

这样检测的就有问题,显示不对

309ba4b9c7b6fc239923e5f191c20dd9.png

a751298777894eeadc64769a45780d16.png

a879cbf5406606fd4dc0069e165ad5de.png

这张比较明显,做了二次截取后只保留了16格的棋盘

88dc65a0a8b9a3c910edc7bd0ae8cc41.png

e129809c6a1ad28b55b96b647bb1d6cc.png

d4146744f6294d952d9955308d622000.png

这张效果是一样的

69fade0bbd13a3603393a9664dc2de0f.png

491b0b2cc5ec823b28b738783e3d9311.png

b3985690311b453fffa334b48208adbd.png

这张也是我想要的效果

8f739c0c041bfefd282c229a45bc9c08.png

1a5233cb1aed353768af167868351853.png

试了多次来说,处理的效果都和预期有差,所以最终考虑了下,还是修改PaddleOCR的源码应该比这个效果好,于是就继续研究PaddleOCR源码。

17d3372d83cfdbda61b072bdfc8f4814.png

首先检测的文本框会存放到boxes的容器中

a9df719828405f7d0e2f3d8f2334fcc5.png

点开RunOCR的函数,遍历boxes的容器后会有个GetRotateCropImage的函数。

40708dd49f49ec7c43daae7b945f53bc.png

在GetRotateCropImage函数中会根据box的点生成截取的图像矩形,所以可以在这里考虑改造函数,将生成的Rect返回来,然后通过坐标和大小来定位及找到对应的识别文本。

7cfcabb773af5d8424aae79cc52a8921.png

其实写这一篇踩坑记录的文章,主要就是用输出来加深自己的印象,学习的会更牢固些,并且在不断地尝试中也会提高自己,就算没有达到自己想要的目录,但是过程中也收获了一些别的东西,只不过花的时间会相对多一些。当然上面说的改PaddleOCR的源码也是给自己留下的新问题,业余的时间会慢慢地接着填坑。这里面封装的透视变换代码还是会继续上传。

源码地址

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

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

4da09166893bbf1f055a77326a74e215.png

扫描二维码

获取更多精彩

微卡智享

b0e1428a5f3fa2c0761291b96390658e.png

「 往期文章 」

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

飞桨PaddleOCR C++预测库布署

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

 

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Vaccae

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

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

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

打赏作者

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

抵扣说明:

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

余额充值