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