前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++ OpenCV检测并提取数字华容道棋盘

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

作者头像
Vaccae
发布2021-07-07 19:12:36
9290
发布2021-07-07 19:12:36
举报
文章被收录于专栏:微卡智享微卡智享

前言

一直关注我的朋友应该知道前段时间使用OpenCV做了数字华容道的游戏及AI自动解题,相关文章《整活!我是如何用OpenCV做了数字华容道游戏!(附源码)》《趣玩算法--OpenCV华容道AI自动解题》,一直也想在现在的基础上再加些东西,就考虑到使用图像读取了棋盘,生成对应的棋局再自动AI解题。

像这样的图像识别,用深度学习的方法实现应该是最佳的,奈何自已也是刚开始自学,很多东西也不太了解,等入门后会更新相关的学习笔记,今天就先用OpenCV传统的方法处理。

Q1

如何实现图像读取数字华容道棋盘生成棋局?

虽然这是一个问题,不过要完成实现需要两个操作,就是定位棋盘和数字识别,那具体应该怎么实现呢?

1.定位并提取数字华容道棋盘(非深度学习方法),今天这篇就是来讲讲怎么实现提取数字华容道棋盘。

2.数字识别(OCR识别),以前文章中有在Android端调用过Tesseract,但PC端一直没装,最近也在看看有没有更合适的框架,所以这块还没定下,等弄好了我们继续做这步。

文中代码只显示核心的代码,文末会有源码的地址,想看源码的可以从地址中下载。

实现效果

#

实现思路

1

图像预处理后进行边缘检测

2

查找到最大的轮廓并且是4边形的轮廓

3

将查找到的轮廓获取到最小旋转矩形进行透视变换

4

提取出透视变换后的图像显示出来

代码实现

微卡智享

01

图像预处理后进行边缘检测

通常进行边缘检测时直接使用Canny边缘检测,因为检测速度也快,《C++ OpenCV使用大津法求自适应阈值》篇中也说过使用大津法求的自适应阈值,开始也是这样用的,后来发现为了检测的效果更好一些,这里采用了把图像R,G,B层分开边缘检测,然后再把三个分开的图像做与操作,最后出来的图像再做处理。

代码语言:javascript
复制
  vector<Mat> channels;
      Mat B_src, G_src, R_src, dstmat;
      split(src, channels);
      
      int minthreshold = 120, maxthreshold = 200;
    
      //B进行Canny
      //大津法求阈值
      CvUtils::GetMatMinMaxThreshold(channels[0], minthreshold, maxthreshold, 1);
      cout << "OTSUmin:" << minthreshold << "  OTSUmax:" << maxthreshold << endl;
      //Canny边缘提取
      Canny(channels[0], B_src, minthreshold, maxthreshold);

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

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

上图中可以看到,中间三个分别是B,G,R三色分别通过Canny边缘求出的图,最右边的是将三个图像与操作后得到的轮廓图。

合并图像显示的代码

代码语言:javascript
复制
      Mat channelmat;
      resize(B_src, B_src, Size(0, 0), 0.4, 0.4);
      resize(G_src, G_src, Size(0, 0), 0.4, 0.4);
      resize(R_src, R_src, Size(0, 0), 0.4, 0.4);
      channelmat.push_back(B_src);
      channelmat.push_back(G_src);
      channelmat.push_back(R_src);
      CvUtils::SetShowWindow(channelmat, "channelmat", 600, 0);
      imshow("channelmat", channelmat);

02

查找到最大的轮廓并且是4边形的轮廓

图像的预处理边缘检测完了,就要开始查找图像中最大轮廓了,因为需要寻找数字华容道的棋盘,所以除了长最大面积外,还要考虑是四边形的轮廓,不是四边形的直接排除即可。找到符合条件的轮廓记录其轮廓编号,用于做下一步处理。

代码语言:javascript
复制
      vector<vector<Point>> contours;
      vector<Vec4i> hierarchy;
      findContours(dstmat, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);

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

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

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

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

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

        //求出最小旋转矩形
        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;
        }
      }

重点说明:

判断轮廓是否是四边形,首先通过计算轮廓的周长再乘0.01得到的值做为阈值,然后通过这个阈值对轮廓的点进行多边形拟合,拟合后的轮廓点个数来判断是不是四边形。

03

取出旋转矩形透视变换并提取

上一步找到符合条件的最大轮廓的编号后,我们单独对这个轮廓进行处理,处理的方式就是《C++ OpenCV透视变换改进---直线拟合的应用》篇中透视变换的改进-----采用直线拟合的方式。

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

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

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

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


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


        //根据最小矩形和多边形拟合的最大四个点计算透视变换矩阵    
        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] = Point2f(left, top);
        rectPoint[1] = rectPoint[0] + Point2f(rWidth, 0);
        rectPoint[2] = rectPoint[1] + Point2f(0, rHeight);
        rectPoint[3] = rectPoint[0] + Point2f(0, rHeight);


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

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

        //载取透视变换后的图像显示出来
        Rect cutrect = Rect(rectPoint[0], rectPoint[2]);
        Mat cutMat = resultimg(cutrect);
        
        CvUtils::SetShowWindow(cutMat, "cutMat", 600, 20);
        imshow("cutMat", cutMat);
      }

上图中根据最小外接矩形找到最近的点进行直接拟合,然后再做透视变换

透视变换后的图像效果

最后在提取出透视变换后我们实际需要的部分

未检测成功的情况

提取的方法这样就说完了,从上面的动图中可以看到,不是所有的图像都提取出来,例如:

上面这张图就是背景太过复杂,边缘检测后找不到合适的轮廓

上图中轮廓检测没问题,但是多边形拟合后得到的轮廓为5个点,

所以不认为是四边形

行人这个肯定检测不出四边形

源码地址

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

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

扫描二维码

获取更多精彩

微卡智享

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微卡智享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 未检测成功的情况
  • 源码地址
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档