opencv制作微信小游戏 最强连一连 辅助(3)--opencv matchTemplete多目标匹配

喜欢ヅ旅行 2023-06-27 03:47 31阅读 0赞

上一篇我写了如何用dfs深度优先搜索算法来求解,入参是一个二维数组,这个二维数组是人为手动赋值的

这一篇我们来讲如何自动来完成这一过程.

也就是说 入参是一个 游戏的画面,出参是一个二维数组

如下图:

在这里插入图片描述

得到了二维数组 我们就可以用dfs算法求解了.


ok ,回到主题
怎么才能 根据图片得到 二维数组地图了?

那就是通过计算机视觉库,这里我们使用opencv

先说下总体的目标吧 ,要想自动的识别地图首先要确认地图最左上角的坐标,
如下图红点
在这里插入图片描述

先看下图像的坐标系

以左上角作为起点,越往右x值越大,越往下y值越大
在这里插入图片描述

游戏方块的大小我们是可以提前测量好的,只要我们知道了地图最左上角的坐标 , 以最上角的坐标为起点,进行for循环遍历,就可以得到每个方块中心的坐标,根据每个方块坐标的颜色,就可以确定这个方块是否可以走
如下图

只要是可以走的路我都画上了绿色的点,不可以走的路我都画上了红色的点.起点我画的是蓝色的点.

在这里插入图片描述
OK,下面来说一说 具体的步骤.

首先确定的是方块的颜色

rgb 的数值 大概是 205 205 205
在这里插入图片描述

再看一下游戏的背景图的颜色

rgb 大概是 39 42 60 左右
在这里插入图片描述

然后最重要的opencv的函数就出现了 matchTemplete,模板匹配

先大概看一下这个函数的参数吧

  1. CV_EXPORTS_W void matchTemplate( InputArray image, InputArray templ,
  2. OutputArray result, int method, InputArray mask = noArray() );

返回值是void,
第一个入参image ,是要匹配的图像也就是我们的游戏画面截图
第二个入参templ,是模板图像,也就是游戏的方块的截图
第三个入参result,是匹配的结果,入参得到函数调用的返回结果,是不是很抓狂.哈哈
第四个入参method,是匹配的算法,这个有很多种的选择

  1. enum TemplateMatchModes {
  2. TM_SQDIFF = 0, //!< \f[R(x,y)= \sum _{x',y'} (T(x',y')-I(x+x',y+y'))^2\f]
  3. TM_SQDIFF_NORMED = 1, //!< \f[R(x,y)= \frac{\sum_{x',y'} (T(x',y')-I(x+x',y+y'))^2}{\sqrt{\sum_{x',y'}T(x',y')^2 \cdot \sum_{x',y'} I(x+x',y+y')^2}}\f]
  4. TM_CCORR = 2, //!< \f[R(x,y)= \sum _{x',y'} (T(x',y') \cdot I(x+x',y+y'))\f]
  5. TM_CCORR_NORMED = 3, //!< \f[R(x,y)= \frac{\sum_{x',y'} (T(x',y') \cdot I(x+x',y+y'))}{\sqrt{\sum_{x',y'}T(x',y')^2 \cdot \sum_{x',y'} I(x+x',y+y')^2}}\f]
  6. TM_CCOEFF = 4, //!< \f[R(x,y)= \sum _{x',y'} (T'(x',y') \cdot I'(x+x',y+y'))\f]
  7. //!< where
  8. //!< \f[\begin{array}{l} T'(x',y')=T(x',y') - 1/(w \cdot h) \cdot \sum _{x'',y''} T(x'',y'') \\ I'(x+x',y+y')=I(x+x',y+y') - 1/(w \cdot h) \cdot \sum _{x'',y''} I(x+x'',y+y'') \end{array}\f]
  9. TM_CCOEFF_NORMED = 5 //!< \f[R(x,y)= \frac{ \sum_{x',y'} (T'(x',y') \cdot I'(x+x',y+y')) }{ \sqrt{\sum_{x',y'}T'(x',y')^2 \cdot \sum_{x',y'} I'(x+x',y+y')^2} }\f]
  10. };

我们使用的是第5个 ,至于这些算法 具体的实现 ,大家可以查找算法仔细研究下.我们就直接使用现成的了.

第五个入参mask,是一个图像,默认是noAyyary(),我们就不管了.

ok 这里我们统一一下图像的名称

输入的游戏截图图像是image
匹配的模板图像也就是方块的图像是templ
匹配的结果图像是result

下面就用变量的名字来代替名称了

这里要讲一下第三个参数result,也就是我们的输出的结果,这个参数的大小有限制,

它的高度是 image的高度- templ的高度+1
它的宽度是 image的宽度-templ的宽度+1

为什么要有这个大小限制了?

这个其实要看matchTemplete匹配的过程,这个函数它大体的匹配过程就是把templ放到image上面 推测是从左往右,从上往下依次匹配的过程,具体的细节需要看算法实现.但是大体如此

如下图红框
其实这个红框所在的矩形范围就是模板图片templ左上角的那个顶点在image中划过的轨迹, 而这个矩形的高度和宽度恰好就是
image的高度- templ的高度+1
image的宽度-templ的宽度+1

所以这个也是为什么第三个入参的result的大小要有这个限制,为什么要说这个了? 下面要用到.

在这里插入图片描述

网上大部分的matchTemplete 函数 都是配合minMaxLoc函数使用 ,结果只能匹配到一个结果

但是我们的地图中有很多的方块啊,怎么可能值匹配一个了?这个怎么办,而且我们想象中的返回结果应该是一个数组 保存x坐标和y坐标.可是他的返回结果是一个Mat 怎么是一个图像?

看了下b站up主的代码 直接是一个numpy.where() 函数 就OK了. 又是一个python神仙语法,这谁顶得住啊.

在这里插入图片描述

没办法,继续研究下,再次回到上面那个输出结果图像的大小问题

我们把这个 结果图像显示出来看看不就行了

左边是 matchTemplete的返回结果的result图像 ,右边是游戏截图image图像

在这里插入图片描述

可以看到 result图像中 是有亮点的,而这些亮点 对比一下 就是对应的是image每个方块的左上角的位置.

越亮的地方 就代表在image图像中以这个点作为左上角和templ图像对齐 匹配度越高,他的数值也是越大,越接近1

而且result中的坐标 体系的值 和 原图 坐标体系的值 是一样的 想想上面的result大小的讨论 ,这个也就解释了 为什么matchTemplete 返回值的结果不是 x和y的数值 而是一个Mat图像. result图像在(x,y)处的值 就代表了,游戏图像image在(x,y)点和模板图像templ的匹配程度.

再次总结一下: 返回结果result的坐标体系和image是一样的,因为result图像本身也是一个二维数组,所以result(x,y)的值 就是代表游戏图像image以(x,y)为起点和模板图像左上角对齐 匹配模板图像的相似度,越靠近1 代表越像,在result图像中也就越亮.

这里大家可以打印出result的值看看,我们选择一个阈值进行过滤,就可以把比较像的点保存下来.这些点大概会有几百或者上千个,远远超过方块的数量,想一想这是为什么.

ok 到这里 我们看上去就获得了所有方块的左上角的坐标.

但是实际使用这个方法 有一定的误差,比较难过滤. 因为有的局部会被匹配成方块,pass ,我们需要换个思路

B站up主使用的方法是 使用minMaxLoc方法找到输入图像中和模板图像最相似的点.

然后以这个点作为扫描的起点,向四周进行扩散扫描,扫描的深度可以自定义 一般10-20差不多 因为二维地图的维度不会太大.

扩散扫描的时候查看扩散点的颜色.

如果颜色是方块的颜色 则把方块的颜色的坐标记录下来,这样就可以得到所有的方块的坐标. 按照这个来统计方块是比较准的,所有的方块都能被正确的识别.

到这里终于确定了所有方块的最上角的坐标

但是我们还没有获取游戏图像image地图的最上角的坐标,这个怎么办了?

首先是找到方块坐标的最大值和最小值
也就是
x坐标最大值 x_max
y坐标最大值 y_max
x最表最小值x_min
y坐标最小值y_min

(x_min,y_min)就是游戏地图最左上角的坐标
(x_max-x_min)/方块长度=二维数组的列数
(y_max-y_min)/方块长度=二维数组的行数

然后就是遍历地图 根据颜色确定二维数组的值了.

ok差不多就是这么多了 ,上代码吧.

  1. #include<iostream>
  2. #include<opencv2/opencv.hpp>
  3. #include<string>
  4. #include <zconf.h>
  5. using namespace std;
  6. using namespace cv;
  7. void buildDfsMap(Mat srcImage, Mat templateImage, Mat result);
  8. int dfsBeginX;
  9. int dfsBeginY;
  10. int blockCount = 0;
  11. int gameMap[10][10];
  12. int n; //x 的长度
  13. int m; //y的长度
  14. int blockSize = 155;
  15. int main() {
  16. Mat templateImage = imread("/Users/saltedfish/Desktop/123.png");
  17. Mat srcImage = imread("/Users/saltedfish/Desktop/789.jpeg");
  18. Mat result;
  19. int result_cols = srcImage.cols - blockSize + 1;
  20. int result_rows = srcImage.rows - blockSize + 1;
  21. result.create(result_cols, result_rows, srcImage.type());
  22. matchTemplate(srcImage, templateImage, result, TM_CCOEFF_NORMED);
  23. buildDfsMap(srcImage, templateImage, result);
  24. }
  25. void buildDfsMap(Mat srcImage, Mat templateImage, Mat result) {
  26. //开始构建地图
  27. //随便获取一个位置 ,这里以最匹配的为开始位置
  28. Point maxPoint;
  29. Point minPoint;
  30. minMaxLoc(result, NULL, NULL, &minPoint, &maxPoint);
  31. //以开始位置为起点,向四周扫描 确定地图的范围
  32. int scanBeginX = maxPoint.x;
  33. int scanBeginY = maxPoint.y;
  34. int mapBeginX = srcImage.cols;
  35. int mapBeginY = srcImage.rows;
  36. int mapEndX = 0;
  37. int mapEndY = 0;
  38. //灰色点三通道的值大概是204 204 204 左右
  39. for (int i = -10; i < 10; i++) {
  40. for (int j = -10; j < 10; j++) {
  41. int dstPointX = scanBeginX + i * blockSize + 0.5 * blockSize;
  42. int dstPointY = scanBeginY + j * blockSize + 0.5 * blockSize;
  43. //超出范围不计
  44. if (dstPointX <= srcImage.cols && dstPointY <= srcImage.rows && dstPointX >= 0 && dstPointY >= 0) {
  45. circle(srcImage, Point(dstPointX, dstPointY), 6, Scalar(0, 0, 255), 8);
  46. int b = srcImage.at<Vec3b>(dstPointY, dstPointX)[0];
  47. int g = srcImage.at<Vec3b>(dstPointY, dstPointX)[1];
  48. int r = srcImage.at<Vec3b>(dstPointY, dstPointX)[2];
  49. if (abs(b - 204) < 5 && abs(g - 204) < 5 && abs(r - 204) < 5) {
  50. circle(srcImage, Point(dstPointX, dstPointY), 6, Scalar(0, 255, 0), 8);
  51. if (mapBeginX >= dstPointX) {
  52. mapBeginX = dstPointX;
  53. }
  54. if (mapBeginY >= dstPointY) {
  55. mapBeginY = dstPointY;
  56. }
  57. if (mapEndX <= dstPointX) {
  58. mapEndX = dstPointX;
  59. }
  60. if (mapEndY <= dstPointY) {
  61. mapEndY = dstPointY;
  62. }
  63. }
  64. }
  65. }
  66. }
  67. m = (mapEndX - mapBeginX) / blockSize;
  68. n = (mapEndY - mapBeginY) / blockSize;
  69. //背景图片的 bgr 57 41 38
  70. for (int i = 0; i <= m; i++) {
  71. for (int j = 0; j <= n; j++) {
  72. int dstPointX = mapBeginX + i * blockSize;
  73. int dstPointY = mapBeginY + j * blockSize;
  74. int b = srcImage.at<Vec3b>(dstPointY, dstPointX)[0];
  75. int g = srcImage.at<Vec3b>(dstPointY, dstPointX)[1];
  76. int r = srcImage.at<Vec3b>(dstPointY, dstPointX)[2];
  77. if (abs(b - 57) < 10 && abs(g - 41) < 10 && abs(r - 38) < 10) {
  78. //背景图片
  79. gameMap[j][i] = 0;
  80. continue;
  81. }
  82. if (abs(b - 204) < 10 && abs(g - 204) < 10 && abs(r - 204) < 10) {
  83. gameMap[j][i] = 1;
  84. blockCount++;
  85. } else {
  86. //起点
  87. circle(srcImage, Point(dstPointX, dstPointY), 6, Scalar(255, 0, 0), 8);
  88. gameMap[j][i] = 2;
  89. dfsBeginX = j;
  90. dfsBeginY = i;
  91. }
  92. }
  93. }
  94. //print Map
  95. for (int i = 0; i <= n; i++) {
  96. for (int j = 0; j <= m; j++) {
  97. cout << gameMap[i][j] << ",";
  98. }
  99. cout << endl;
  100. }
  101. imwrite("/Users/saltedfish/Desktop/gameMap.png", srcImage);
  102. }

运行这个代码 需要本地配置好opencv的环境

ok,看一下运行的效果吧

在这里插入图片描述

哈哈哈 ,成功了
在这里插入图片描述

得到了游戏地图的二维数组,又有了dfs算法求解,我们就可以求出解了.

但是还有一个问题 就是 虽然计算机虽然知道怎么走了,但是计算机不能去像人一样去触摸屏幕啊

没事,好在有解决方法.那就是通过 adb 模拟 触摸

ok,我们下一章再见.

发表评论

表情:
评论列表 (有 0 条评论,31人围观)

还没有评论,来说两句吧...

相关阅读