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

2021-07-07 19:12:36 浏览数 (1)

前言

一直关注我的朋友应该知道前段时间使用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上不去的朋友,可以击下方的原文链接跳转到码云的地址,关注【微卡智享】公众号,回复【源码】可以下载我的所有开源项目。

扫描二维码

获取更多精彩

微卡智享

0 人点赞