为了网络安全课程的实训作业检查,写了这个程序,思路和代码有一部分来自网上,然后再通过自己的思路和实践实现的,有学到一些知识,也碰到很多问题,在博客里分享一下,希望可以帮到有需要的人。
效果图片
Gif动图的制作软件用的是ScreenToGif
环境配置
-
安装pycharm
这里要记得为系统配置python的环境变量,以后就可以直接在cmd里面使用pip命令安装其他的函数库了 -
使用pip命令安装函数库
接下来的程序预计会需要下载三个额外的函数库:
pyautogui(鼠标键盘的自动化操作)
pillow(处理图像)
numpy(numpy的二维数组,用于存储整张连连看图片的数据信息)
在cmd中输入命令:pip list 可以查看已经已经安装的库。
-
pycharm环境
当你安装好库之后,并不能直接引用,需要在pycharm里面对编译环境经行配置引入之前安装好的包。先点击pycharm右下角的interpreter setting
点击加号
搜索自己想添加的库,然后点击install package,安装成功后既可以直接使用了。
设计思路
程序需要人工传入图片左上角和右下角的坐标(pos1,pos2),小图片的行数(row),列数(col),即可运行程序。
首先使用ImageGrab传入坐标截取整张大图片,由于知道小图片的行数,列数,使用.crop方法就可以把大图分割成小图片,对于每两张小图片使用计算dhash值对比汉明距离原理讲解(opencv库代码实现),本片博客用的是pillow库实现dhash来得到两张图片的相似度(因为dhash精准快速),相同的图片存为同一种数字存入numpy的二维数字矩阵中,空白部分为通路标记为0,因为连连看是可以走外圈的,所以最外层的空白部分标记为0,连连看只有两张同样的图片并且其连线不能转弯超过两次即认为可消去。枚举所有两张相同小图片的坐标使用dfs深度优先搜索判断转弯次数即可判断两张图片是否可以消去,判断两张图片是否可以消去的方法有很多,详情请参考这一题的解法HDU 1728逃离迷宫。判断如果图片可以消去那么就把图片的数字重置为0,并且调用pyautogui自动操作鼠标点击图片的位置,在游戏中消去图片,如果的到的结果是不可消去就继续寻找其他的图片,直到将所有图片消去为止。
原图片:
数组矩阵:
观察上面两张图片,我们可以知道经过处理之后一样的图片数字会是一样的,对截图的精准程度要求很高,如果图片种类识别一直不精准,可以尝试修改代码里 不匹配度Mismatch 以及 压缩边框长度cor 这两个代码的值来尝试使图片种类数正确。
个人实现过程中的问题(必看)
- 快速获取像素点
程序需要传入游戏界面的大图片位置pos1和pos2,pyautogui有函数可显示当前鼠标位置,程序显示鼠标位置的方法很麻烦,每次需要运行一次鼠标然后运行一次程序,而且人手把鼠标移动到某个像素点位置精度是不准确的,会导致后面切割小图片,以及小图片的相似度对比出错。所以建议使用取色器,会很精准。可以放到一个大概的位置,然后通过上下左右键移动来获取精准的左上角右下角坐标。我用了这个取色器snipaste取色器
- 图片截取精度
在测试过程中还碰到一个问题,就是在精准传入左上,右下角坐标和小图片行数,列数之后还是无法准确对比图片,当时导致我很纳闷,去自习检查了图片相似度的对比算法,也没有任何问题。后输出截取好的小图片才发现,原来是因为小图片的分割条那里有问题。
当时测试的某一款连连看游戏的行列分割栏的粗细不一样,而且还没有规律,不是图片里的这一款,一般来说分割线的粗细都是一致的,只是当时不太走运,就导致截取的图片不均匀,有些图片有黑色边框,最后想了一个解决的办法就是置截取中心部分。就可以消除黑色边框导致的图片相似匹配不准确。
红色矩形内为实际截取部分。
详解源代码
import pyautogui
from PIL import ImageGrab, Image
import numpy as np
def ClickPos(x,y):#鼠标移动到(x,y)并且点击
pyautogui.moveTo(x,y,0.001)#鼠标在0.001秒内移动到(x,y)
pyautogui.click()#点击鼠标当前位置
return
def cal_posX(x):return (x- 0.5) * pic_height + pos1y#通过小图片所在行数计算出在实际屏幕的像素点y坐标
def cal_posY(y):return (y- 0.5) * pic_len + pos1x #通过小图片所在列数计算出在实际屏幕的像素点x坐标
def getDhash(im):#得到dhash串
im_gray = np.array(im.resize((9, 8), Image.ANTIALIAS).convert('L'), 'f')#压缩图片为(9,8)大小,并且转为灰度图,并且使用np.array转成像素数组
hash_str=''#dhash串
for i in range(8):
for j in range(8):
if im_gray[i,j]>im_gray[i,j+1]:
hash_str+= '1'#如果[i,j]像素点大于[i,j+1]就生成hash值1,否则生成hash值0
else:
hash_str+= '0'
return hash_str
def isMatch(hash1, hash2):
mismatch=0#不匹配度,两个01hash串相同位置不同字符的个数
for i in range(len(hash1)):#遍历长度
if hash1[i]!=hash2[i]:
mismatch+=1
return mismatch < 16#不匹配度很小时,认为两张图片相同,有时候需要自己手动调,太高或者太低都会导致图片种类识别错误
def getIndex(im, im_list):#得到该图片的下标
for i in range(len(im_list)):#遍历图片列表,查看图片是否存在
if isMatch(getDhash(im), getDhash(im_list[i])):
return i#如果存在就返回图片的下标
return -1#不存在返回-1
def valid(st):#dfs判断点移动到的位置是否合法
if(0<=st[0]<=row+1 and 0<=st[1]<=col+1 and arr[st[0]][st[1]]==0 ):return True
return False
def isConnectable(st,e,dir): #参数:起点,终点,上次转弯方向
step=vis[st[0]][st[1]] #当前拐弯次数
if(st==e and step<=maxTurns):#从起点到达终点并且拐弯次数少于两次
global flag#声明全局变量,python在函数里改变变量值需要申明,但项列表或者numpy矩阵就不需声明就能直接修改其元素的值
flag = True
return
if((st != e and step == maxTurns)or step>maxTurns):return#减枝,未到终点,拐弯2次,或者超过两次拐弯都可以结束当前的递归
for value in direct: #在四个方向上生成新的节点
x,y = st[0] + value[0] , st[1] + value[1]#在四个方向上生成新的节点
next_st = [x, y]#新节点
if (not valid(next_st)):#新节点不合法直接进入下一次循环
continue
if(vis[x][y]<step):continue#拐弯次数超过直接进入下一次循环
if (dir!=[0,0] and dir != value and vis[x][y] < step + 1):
continue#如果不是初始方向,并且上次方向和这次不一样(需要拐弯),但是拐弯次数不小于超过(x,y)就没必要进入(x,y)点,因为其他格子已经有更优的路径
if (dir!=[0,0] and dir != value):#如果不是初始方向,并且上次方向和这次不一样(需要拐弯)拐弯次数+1
vis[x][y] = step + 1
else:
vis[x][y] = step#不需要拐弯,拐弯次数不变
arr[x][y]=1 #将走过的路标记,避免来回走陷入死循环
isConnectable(next_st, e, value)
arr[x][y]=0 #回溯变成初始状态
if(flag):return
def getArr():#截取大图片,返回小图片,图片处理后返回二维数字矩阵
arr = np.zeros((row + 2, col + 2), dtype=np.int32) #数字矩阵,外围设为0,并且初始化为0
image_type_list = [] # 列表去重后的所有图片种类
image_list = {}#小图片元组
image = ImageGrab.grab((pos1x, pos1y, pos2x, pos2y)) # 截图获取整张图片
for x in range(row):#将图片分割成小块存入二维数组
image_list[x] = {}#将x行全部赋值为空
for y in range(col):
left = y * pic_len + cor
top = x * pic_height + cor
right = (y + 1) * pic_len - cor
bottom = (x + 1) * pic_height - cor
im = image.crop((left, top, right, bottom))# 用crop函数切割成小图标,参数为图标的左上角和右下角左边(在整张大图片中的位置)
image_list[x][y] = im # 将切割好的图标存入对应的位置
for i in range(row):#将图片数字化
for j in range(col):
im = image_list[i][j]#识别出不同的图片,将图片矩阵转换成数字矩阵
index = getIndex(im,image_type_list)# index验证当前图标是否已存入
if index < 0:
image_type_list.append(im)
arr[i + 1][j + 1] = len(image_type_list)
else:
arr[i + 1][j + 1] = index + 1
return arr
###全局变量
flag=False#两张相同图片是否可以联通,初始换为Flase
pos1x,pos1y,pos2x,pos2y=354, 303, 1457, 1018#左上角坐标,右下角坐标
row,col=10,14 #行数,列数
pic_len= (pos2x - pos1x) / col #图片长
pic_height= (pos2y - pos1y) / row #图片高
total=row*col#图片总数
cor=10#小图片边框误差消除误差
direct=[[1,0],[-1,0],[0,1],[0,-1]]#搜索时可行走的四个方向
maxTurns =2#连连看情况下的最大拐弯数
vis = np.full((row+2, col+2), maxTurns + 10)#记录拐弯的次数,初始化为比拐弯次数更大的数
###全局变量
####玩游戏过程
ClickPos(1691,17)#点击缩小键,缩小当前pycharm编辑器,运行程序之前需要把游戏窗口先打开,其实也可以用类似selenium这样的自动化库来打开,但是懒得写了。。。
arr=getArr()#得到图片的二维数字矩阵
print(arr)
Maxcnt=0#防止图片识别不精准时,程序死循环
while total>0:
Maxcnt+=1
if(Maxcnt>300):exit()#超过300次循环结束程序
for xi in range(1,row+1):
for yi in range(1, col + 1):#枚举所有起点
if(arr[xi][yi]==0):continue#如果该点已被消去就跳过
for xj in range(1, row + 1):
for yj in range(1, col + 1):#枚举所有终点
if (arr[xi][yi]!=arr[xj][yj] or arr[xj][yj] == 0 or [xi,yi]==[xj,yj]): continue#起点终点不能是被消去的点,而且必须是同一张图片
temp_v=arr[xj][yj]
arr[xj][yj]=0 #j作为终点,暂且变成空白,变成通路
vis = np.full((row + 2, col + 2), maxTurns + 10)#将标记拐弯步数的数组赋值为最大值
vis[xi][yi]=0#起点的拐弯数赋值为0
isConnectable([xi, yi], [xj, yj], [0, 0])#判断两点是否可连接参数:起点坐标,终点坐标,【0,0】为初始方向
if(flag):#如果可以连接
flag=False
total-=2#总数减去被消去的两块
arr[xi][yi]=0#把起点的值赋值为0,意思是以消去
print(xi,yi,xj,yj)
ClickPos(cal_posY(yi),cal_posX(xi))
ClickPos(cal_posY(yj),cal_posX(xj))#点击两点在屏幕的实际位置
else:
arr[xj][yj]=temp_v#如果两点不可连接就复原终点的值
print(arr)
###玩游戏过程