opencv制作微信小游戏 最强连一连 辅助(3)--opencv matchTemplete多目标匹配
上一篇我写了如何用dfs深度优先搜索算法来求解,入参是一个二维数组,这个二维数组是人为手动赋值的
这一篇我们来讲如何自动来完成这一过程.
也就是说 入参是一个 游戏的画面,出参是一个二维数组
如下图:
得到了二维数组 我们就可以用dfs算法求解了.
ok ,回到主题
怎么才能 根据图片得到 二维数组地图了?
那就是通过计算机视觉库,这里我们使用opencv
先说下总体的目标吧 ,要想自动的识别地图首先要确认地图最左上角的坐标,
如下图红点
先看下图像的坐标系
以左上角作为起点,越往右x值越大,越往下y值越大
游戏方块的大小我们是可以提前测量好的,只要我们知道了地图最左上角的坐标 , 以最上角的坐标为起点,进行for循环遍历,就可以得到每个方块中心的坐标,根据每个方块坐标的颜色,就可以确定这个方块是否可以走
如下图
只要是可以走的路我都画上了绿色的点,不可以走的路我都画上了红色的点.起点我画的是蓝色的点.
OK,下面来说一说 具体的步骤.
首先确定的是方块的颜色
rgb 的数值 大概是 205 205 205
再看一下游戏的背景图的颜色
rgb 大概是 39 42 60 左右
然后最重要的opencv的函数就出现了 matchTemplete,模板匹配
先大概看一下这个函数的参数吧
CV_EXPORTS_W void matchTemplate( InputArray image, InputArray templ,
OutputArray result, int method, InputArray mask = noArray() );
返回值是void,
第一个入参image ,是要匹配的图像也就是我们的游戏画面截图
第二个入参templ,是模板图像,也就是游戏的方块的截图
第三个入参result,是匹配的结果,入参得到函数调用的返回结果,是不是很抓狂.哈哈
第四个入参method,是匹配的算法,这个有很多种的选择
enum TemplateMatchModes {
TM_SQDIFF = 0, //!< \f[R(x,y)= \sum _{x',y'} (T(x',y')-I(x+x',y+y'))^2\f]
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]
TM_CCORR = 2, //!< \f[R(x,y)= \sum _{x',y'} (T(x',y') \cdot I(x+x',y+y'))\f]
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]
TM_CCOEFF = 4, //!< \f[R(x,y)= \sum _{x',y'} (T'(x',y') \cdot I'(x+x',y+y'))\f]
//!< where
//!< \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]
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]
};
我们使用的是第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差不多就是这么多了 ,上代码吧.
#include<iostream>
#include<opencv2/opencv.hpp>
#include<string>
#include <zconf.h>
using namespace std;
using namespace cv;
void buildDfsMap(Mat srcImage, Mat templateImage, Mat result);
int dfsBeginX;
int dfsBeginY;
int blockCount = 0;
int gameMap[10][10];
int n; //x 的长度
int m; //y的长度
int blockSize = 155;
int main() {
Mat templateImage = imread("/Users/saltedfish/Desktop/123.png");
Mat srcImage = imread("/Users/saltedfish/Desktop/789.jpeg");
Mat result;
int result_cols = srcImage.cols - blockSize + 1;
int result_rows = srcImage.rows - blockSize + 1;
result.create(result_cols, result_rows, srcImage.type());
matchTemplate(srcImage, templateImage, result, TM_CCOEFF_NORMED);
buildDfsMap(srcImage, templateImage, result);
}
void buildDfsMap(Mat srcImage, Mat templateImage, Mat result) {
//开始构建地图
//随便获取一个位置 ,这里以最匹配的为开始位置
Point maxPoint;
Point minPoint;
minMaxLoc(result, NULL, NULL, &minPoint, &maxPoint);
//以开始位置为起点,向四周扫描 确定地图的范围
int scanBeginX = maxPoint.x;
int scanBeginY = maxPoint.y;
int mapBeginX = srcImage.cols;
int mapBeginY = srcImage.rows;
int mapEndX = 0;
int mapEndY = 0;
//灰色点三通道的值大概是204 204 204 左右
for (int i = -10; i < 10; i++) {
for (int j = -10; j < 10; j++) {
int dstPointX = scanBeginX + i * blockSize + 0.5 * blockSize;
int dstPointY = scanBeginY + j * blockSize + 0.5 * blockSize;
//超出范围不计
if (dstPointX <= srcImage.cols && dstPointY <= srcImage.rows && dstPointX >= 0 && dstPointY >= 0) {
circle(srcImage, Point(dstPointX, dstPointY), 6, Scalar(0, 0, 255), 8);
int b = srcImage.at<Vec3b>(dstPointY, dstPointX)[0];
int g = srcImage.at<Vec3b>(dstPointY, dstPointX)[1];
int r = srcImage.at<Vec3b>(dstPointY, dstPointX)[2];
if (abs(b - 204) < 5 && abs(g - 204) < 5 && abs(r - 204) < 5) {
circle(srcImage, Point(dstPointX, dstPointY), 6, Scalar(0, 255, 0), 8);
if (mapBeginX >= dstPointX) {
mapBeginX = dstPointX;
}
if (mapBeginY >= dstPointY) {
mapBeginY = dstPointY;
}
if (mapEndX <= dstPointX) {
mapEndX = dstPointX;
}
if (mapEndY <= dstPointY) {
mapEndY = dstPointY;
}
}
}
}
}
m = (mapEndX - mapBeginX) / blockSize;
n = (mapEndY - mapBeginY) / blockSize;
//背景图片的 bgr 57 41 38
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
int dstPointX = mapBeginX + i * blockSize;
int dstPointY = mapBeginY + j * blockSize;
int b = srcImage.at<Vec3b>(dstPointY, dstPointX)[0];
int g = srcImage.at<Vec3b>(dstPointY, dstPointX)[1];
int r = srcImage.at<Vec3b>(dstPointY, dstPointX)[2];
if (abs(b - 57) < 10 && abs(g - 41) < 10 && abs(r - 38) < 10) {
//背景图片
gameMap[j][i] = 0;
continue;
}
if (abs(b - 204) < 10 && abs(g - 204) < 10 && abs(r - 204) < 10) {
gameMap[j][i] = 1;
blockCount++;
} else {
//起点
circle(srcImage, Point(dstPointX, dstPointY), 6, Scalar(255, 0, 0), 8);
gameMap[j][i] = 2;
dfsBeginX = j;
dfsBeginY = i;
}
}
}
//print Map
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
cout << gameMap[i][j] << ",";
}
cout << endl;
}
imwrite("/Users/saltedfish/Desktop/gameMap.png", srcImage);
}
运行这个代码 需要本地配置好opencv的环境
ok,看一下运行的效果吧
哈哈哈 ,成功了
得到了游戏地图的二维数组,又有了dfs算法求解,我们就可以求出解了.
但是还有一个问题 就是 虽然计算机虽然知道怎么走了,但是计算机不能去像人一样去触摸屏幕啊
没事,好在有解决方法.那就是通过 adb 模拟 触摸
ok,我们下一章再见.
还没有评论,来说两句吧...