先上知乎上大神所写:

你看过/写过哪些有意思的代码?

然后别急着看blog,玩一把游戏再说!

看知乎评论,有人说他用了一个下午时间就写出来了. wo kao!!! 我断断续续写了一周的下午才搞定,然后又用了4个小时将近一个下午才将代码搬到博客园. 要说这个自动连接连连看说简单也简单,说不简单也不简单.反正是没你想的那么简单也没你想的那么复杂(喜欢说废话的人).做这个小程序遇到了好几个问题,一个一个搞定还蛮有成就感.

我也先放一个视频来看看程序跑起来的效果,(话说博客园不能上传视频,只能上传swf) mp4 转swf 失真的太厉害啦! 就忍着这样看吧.......

连连看的算法

连连看顾名思义, 将相同的图案连接起来,规则是什么呢? 很简单, 在三条线以内完成连接即可,换句话说,在小于等于两次拐角的情况下能把图案相同的连接起来就是可联通点.所以思路很清晰了,只要满足条件就能联通,3条线以内可分三种情况.

情况一:直线连接, 也就是两个点同在X轴 或同在Y轴, 这种情况下,我们先判断是在X轴还是在Y轴, 然后计算出同一个轴上的最大值与最小值,最大值MAX减去最小值Min就是两个点之间的距离,然后在判断两点之间的距离是否为空,为空即为可联通的点,不为空表示之间有其他不同的图案点.

情况二:一次拐角连接, 一次拐角就相当于是用了两条线进行联通,也就是两个点肯定不在同一个X轴或肯定不在同一个Y轴,那两个点的沿(长)线上肯定会有交集, 而且肯定是两个交集, 一个是点1的X轴与点2的Y轴交集 暂叫该点为点C, 另外一个是点1的Y轴与点2的X轴 暂叫该点为点D,那问题就很简单了,一次拐角的连接其实就是两个直线的连接, 只是这个拐角的连接点变成了两点直接的两个交集, 只要计算两点与交集是否能同时联通, 同时联通就认为一次拐角可联通.比如:点1与C可联通,但点C与点2不可联通,视为不可联通, 如果点1与点D可联通,且点D与点2可联通,即视为点1与点2一次拐角可联通.

情况三:两次拐角连接,刚才说过一次拐角连接的情况其实就是两个直线连接的and情况, 那两次拐角连接是不是三个直线的and情况呢, 对的,事实就是这样的,只不过一次拐角连接的情况我们能知道两点的交集点,也就是上面刚才所说的点C与点D,两次拐角的情况我们就不知道这个点在哪了,所以我们就要先选择一个点, 就它了---->点1,   遍历点1在一条直线上的点,这条直线可能是在点1的X轴上,有可能在点1的Y轴上 ,不管如何,遍历出的这个点肯定在点1的X轴或Y轴上,否则与点1就不是一条直线了,假设我们遍历点1直线上的点现在取到了该点为点E, 问题就变成点1与点E是否直线可联通, 点E与点2是否一次拐角可联通,点E与点2是否一次拐角可联通可以直接用情况二的算法. 当满足一个直线,一个拐角 and (且) 情况下即为联通,否则继续遍历寻找点E,直到满足条件.

坎坷的实现过程

再来说说实现的过程

selenium+PIL 获取网页flash图像

网页flash游戏来自:

http://www.4399.com/flash/24238_2.htm

这种方案是因为哥用的Ubuntu,应用商店几乎没有好玩点的连连看,H5, flash肯定是首选,那通过PC玩游戏更是首选.

参考:

利用selenium实现验证码获取并验证   https://zhuanlan.zhihu.com/p/25171554

selenium+python实现1688网站验证码图片的截取   https://blog.csdn.net/zwq912318834/article/details/78605486

selenium获取位置,通过PIL保存截图,分析相同图像,在通过click事件触发自动完成连连看, OK,上代码:

  1. # 前提 程序启动时 对浏览器窗口大小 设置
  2. '''
  3. https://zhuanlan.zhihu.com/p/25171554
  4. https://blog.csdn.net/zwq912318834/article/details/78605486
  5. '''
  6. from selenium import webdriver
  7. import time
  8. from PIL import Image
  9.  
  10. chromeOpitons = webdriver.ChromeOptions()
  11. prefs = {
  12. "profile.managed_default_content_settings.images": 1,
  13. "profile.content_settings.plugin_whitelist.adobe-flash-player": 1,
  14. "profile.content_settings.exceptions.plugins.*,*.per_resource.adobe-flash-player": 1,
  15. }
  16. chromeOpitons.add_experimental_option('prefs', prefs)
  17. time.sleep(5)
  18.  
  19. # 打开
  20. driver = webdriver.Chrome()
  21. url = "http://www.4399.com/flash/24238_2.htm"
  22. driver.set_window_size(1200, 800)
  23. driver.get(url)
  24.  
  25. time.sleep(15)
  26. # 获取截图
  27. driver.get_screenshot_as_file('screenshot.png')
  28.  
  29. # 获取指定元素位置
  30. element = driver.find_element_by_id('swfdiv')
  31. left = int(element.location['x'])
  32. top = int(element.location['y'])
  33.  
  34. print(left, top)
  35. print(element.size['width'], element.size['height'])
  36. right = int(element.location['x'] + element.size['width'])
  37. bottom = int(element.location['y'] + element.size['height'])
  38.  
  39. # 通过Image处理图像
  40. im = Image.open('screenshot.png')
  41. im = im.crop((left, top, right, bottom))
  42. im.save('flash_game.png')

问题来了,chrome浏览器支持flash,但是当webdriver 控制浏览器的时候,flash死活不能启用! Firefox也是一样!

           

后来当我写这个blog的时候又search 查原因,尝试了下, 居然可以了!不过已经用本地player实现了, 有时间了在写这个webdriver 控制浏览器截图+自动click

说一下当时为什么flash在浏览器中可以启用, 在webdriver控制中就不能使用, 一行代码解决,自行体会

  1. sudo apt-get install pepperflashplugin-nonfree

当时放弃了selenium 控制浏览器的方法后,就把flash.swf下载下来,通过 gnash flash播放器打开swf文件,PIL截图,切图,对比图片,pyautogui模拟鼠标点击.......的思路完成自动连连看.思路有了,就一步一步去实现.

PIL截图 切图

查找相似的图片就是把每个小方块都切割出来进行对比,PIL和openCV都可以做到,这里只提PIL.

PIL截图很容易Image.save 方法就是保存操作,切图使用Image.crop方法,crop有四个元组的参数表示为坐标 (left, upper, right, lower), right-left 与lower-upper 计算的大小就是图片的像素大小.

例如下面这张图片,切成4X4的小图片  (源图片的大小为 840X1280)

  1. from PIL import Image
  2.  
  3. img = Image.open('resource.jpg') # 固定图片的大小为 840X1280
  4. region = (210, 0, 420, 320) # rop((x0,y0,x1,y1)) 图片裁切
  5.  
  6. for i in range(0, 4): # width长
  7. for j in range(0, 4): # height高
  8.  
  9. r = (i * 210, j * 320, 210 + 210 * i, 320 + 320 * j)
  10. # 裁切图片
  11. cropImg = img.crop(r)
  12. # 保存裁切后的图片
  13. cropImg.save('crop' + str(i) + str(j) + '.jpg')
  14. print(r)

切割后就生成了12个小图片,以下是12个小图片拼接的效果

其实,在这个程序中,也可以不用保存小方块, 直接将切好的图片对象放在list中,用的时候直接从内存读取. 在程序中生成是便于理解.ok,知道了PIL如何截图和切图,我们现在模拟打开flash播放器,截取屏幕中的游戏区域与连连看的小方块区域,并生成小方块.

在这之前,我们要知道flash的大小,游戏区域与屏幕距离大小, 连连看的小方块区域与flash播放器的高度与左侧空间区域大小,每个小方块像素大小.

上代码:

  1. import pyscreenshot as ImageGrab
  2. import os
  3. import time
  4. from PIL import Image
  5.  
  6. os.popen('gnash a.swf -j750 -k450 -X100 -Y100')
  7. print('打开gnash swf 完成')
  8.  
  9. time.sleep(5)
  10. im = ImageGrab.grab([100, 100, 850, 580]) # X1,Y1,X2,Y2 (X2-X1=750,Y2-Y1=450) # 通过截图工具查看为750*480
  11. im.save("1.png")
  12.  
  13. # 切片
  14. # 通过Image处理图像
  15. im = Image.open('1.png')
  16. left = 80
  17. top = 145
  18. im = im.crop((left, top, 560 + left, 320 + top))
  19. im.save('2.png')
  20.  
  21. # 贴片成小方块
  22. img = Image.open('2.png')
  23. for y in range(0, 8): # height高
  24.  
  25. for x in range(0, 14): # width长
  26. r = (x * 40, y * 40, 40 + 40 * x, 40 + 40 * y)
  27. cropImg = img.crop(r)
  28. x_play = x + 1
  29. y_play = abs(8 - y)
  30.  
  31. cropImg.save('./openpic_o/x' + str(x_play) + "y" + str(y_play) + '.png')
  32. cropImg = cropImg.crop((5, 5, 35, 35)) # 生成的40*40 左右上下裁剪5px
  33.  
  34. cropImg.save('./openpic/x' + str(x_play) + "y" + str(y_play) + '.png')
  35. time.sleep(0.01)
  36. print(r)

打印的内容:

  1. 打开gnash swf 完成
  2. (0, 0, 40, 40)
  3. (40, 0, 80, 40)
  4. (80, 0, 120, 40)
  5. (120, 0, 160, 40)
  6. (160, 0, 200, 40)
  7. (200, 0, 240, 40)
  8. (240, 0, 280, 40)
  9. (280, 0, 320, 40)
  10. (320, 0, 360, 40)
  11. (360, 0, 400, 40)
  12. (400, 0, 440, 40)
  13. (440, 0, 480, 40)
  14. (480, 0, 520, 40)
  15. (520, 0, 560, 40)
  16. (0, 40, 40, 80)
  17. (40, 40, 80, 80)
  18. (80, 40, 120, 80)
  19. (120, 40, 160, 80)
  20. (160, 40, 200, 80)
  21. (200, 40, 240, 80)
  22. (240, 40, 280, 80)
  23. (280, 40, 320, 80)
  24. (320, 40, 360, 80)
  25. (360, 40, 400, 80)
  26. (400, 40, 440, 80)
  27. (440, 40, 480, 80)
  28. (480, 40, 520, 80)
  29. (520, 40, 560, 80)
  30. (0, 80, 40, 120)
  31. (40, 80, 80, 120)
  32. (80, 80, 120, 120)
  33. (120, 80, 160, 120)
  34. (160, 80, 200, 120)
  35. (200, 80, 240, 120)
  36. (240, 80, 280, 120)
  37. (280, 80, 320, 120)
  38. (320, 80, 360, 120)
  39. (360, 80, 400, 120)
  40. (400, 80, 440, 120)
  41. (440, 80, 480, 120)
  42. (480, 80, 520, 120)
  43. (520, 80, 560, 120)
  44. (0, 120, 40, 160)
  45. (40, 120, 80, 160)
  46. (80, 120, 120, 160)
  47. (120, 120, 160, 160)
  48. (160, 120, 200, 160)
  49. (200, 120, 240, 160)
  50. (240, 120, 280, 160)
  51. (280, 120, 320, 160)
  52. (320, 120, 360, 160)
  53. (360, 120, 400, 160)
  54. (400, 120, 440, 160)
  55. (440, 120, 480, 160)
  56. (480, 120, 520, 160)
  57. (520, 120, 560, 160)
  58. (0, 160, 40, 200)
  59. (40, 160, 80, 200)
  60. (80, 160, 120, 200)
  61. (120, 160, 160, 200)
  62. (160, 160, 200, 200)
  63. (200, 160, 240, 200)
  64. (240, 160, 280, 200)
  65. (280, 160, 320, 200)
  66. (320, 160, 360, 200)
  67. (360, 160, 400, 200)
  68. (400, 160, 440, 200)
  69. (440, 160, 480, 200)
  70. (480, 160, 520, 200)
  71. (520, 160, 560, 200)
  72. (0, 200, 40, 240)
  73. (40, 200, 80, 240)
  74. (80, 200, 120, 240)
  75. (120, 200, 160, 240)
  76. (160, 200, 200, 240)
  77. (200, 200, 240, 240)
  78. (240, 200, 280, 240)
  79. (280, 200, 320, 240)
  80. (320, 200, 360, 240)
  81. (360, 200, 400, 240)
  82. (400, 200, 440, 240)
  83. (440, 200, 480, 240)
  84. (480, 200, 520, 240)
  85. (520, 200, 560, 240)
  86. (0, 240, 40, 280)
  87. (40, 240, 80, 280)
  88. (80, 240, 120, 280)
  89. (120, 240, 160, 280)
  90. (160, 240, 200, 280)
  91. (200, 240, 240, 280)
  92. (240, 240, 280, 280)
  93. (280, 240, 320, 280)
  94. (320, 240, 360, 280)
  95. (360, 240, 400, 280)
  96. (400, 240, 440, 280)
  97. (440, 240, 480, 280)
  98. (480, 240, 520, 280)
  99. (520, 240, 560, 280)
  100. (0, 280, 40, 320)
  101. (40, 280, 80, 320)
  102. (80, 280, 120, 320)
  103. (120, 280, 160, 320)
  104. (160, 280, 200, 320)
  105. (200, 280, 240, 320)
  106. (240, 280, 280, 320)
  107. (280, 280, 320, 320)
  108. (320, 280, 360, 320)
  109. (360, 280, 400, 320)
  110. (400, 280, 440, 320)
  111. (440, 280, 480, 320)
  112. (480, 280, 520, 320)
  113. (520, 280, 560, 320)

再看同级目录下有两张图片,1.png与2.png

    

在openpic_o目录,就可以看到裁剪后的小方块,方块大小就是原小方块大小 40*40像素的.

在openpic目录, 看到的是在小方块的基础之上,又裁剪了一次,把小方块的上 下 左 右 各边框都裁剪了5个像素,为什么要这样操作呢? 因为40*40px的图片即使flash引用的相同图片,但因分辨率问题,PIL裁剪后也有可能出现毛边,为了排除这个干扰才又裁剪了一次.如下图

     

关于坐标

有没有发现上面代码中有这么两行代码

  1. x_play = x + 1
  2. y_play = abs(8 - y)

为什么是这样呢?我们先来看下图, 下图是我们平时用的平面二位直角坐标,是从中心点(0,0)开始, 横向左为负数,横向右为正数,纵轴下为负数,纵轴上为正数,但是电脑上的坐标都是左上角(0,0)为坐标系,为了便于理解,我们就把Y轴进倒置,因为高度有8个小方块,所以就是8-y的绝对值.至于x轴,因为从左到右也是依次增大,so,X轴不变.

但为什么又是  x_play = x + 1  先看生成的小方块,小方块的名称是从 x1y1, x1y2,x1y3....x2y1...... 也就是从1开始,如果(1,1)就是起始的坐标点的话,四个边框的点坐标就只能直线连接或从里面消除点后再通过里面点进行迂回连接,这样便一开始就出问题了,因为四个边框的点是可以通过两个拐角连接的,换句话说,四个边框的外围还有一圈空的点,这些空的点坐标是可以联通的,所以才能构成边框上的点联通.也就有了(0,1),(1,0),(0,2),(0,3)...等坐标了,这里的x+1,也就是直接初试从(1,1)开始,给空的坐标点做预留.看下图.

我们定义link_ponts 为可联通的点坐标

  1. link_points = [] # 可联通点坐标
  2. for x in range(0, 14 + 2): # 网格个数+2 可以认为网格小方块的外围有一圈空的方块
  3. for y in range(0, 8 + 2): # 网格个数+2
  4. load_name = "x" + str(x) + "y" + str(y)
  5. if not os.path.exists("./openpic/" + load_name + '.png'):
  6. link_points.append(load_name)
  7.  
  8. print(link_points)

打印link_points

  1. ['x0y0', 'x0y1', 'x0y2', 'x0y3', 'x0y4', 'x0y5', 'x0y6', 'x0y7', 'x0y8', 'x0y9', 'x1y0', 'x1y9', 'x2y0', 'x2y9', 'x3y0', 'x3y9', 'x4y0', 'x4y9', 'x5y0', 'x5y9', 'x6y0', 'x6y9', 'x7y0', 'x7y9', 'x8y0', 'x8y9', 'x9y0', 'x9y9', 'x10y0', 'x10y9', 'x11y0', 'x11y9', 'x12y0', 'x12y9', 'x13y0', 'x13y9', 'x14y0', 'x14y9', 'x15y0', 'x15y1', 'x15y2', 'x15y3', 'x15y4', 'x15y5', 'x15y6', 'x15y7', 'x15y8', 'x15y9']

相似图片分组

这一步骤, 就要提到昨天写的blog

Python OpenCV 图像相识度对比

图片的相似度不管知乎大神用的是

cv2.subtract

还是

numpy.subtract

我这里处理的小方块都有问题,很简单的(x1y2.png),(x2y4.png), (x2y6.png)识别就有问题.说x1y2.png与x2y6.png不相等,我就笑了.

(x1y2.png)    (x2y4.png)     (x2y6.png)

后来我就用了openCV图像的相似度对比,哈希算法也行,灰度RGB通道直方图算法匹配也行, 但是感觉通道直方图计算出来的值更准确, 只能说没有更好的,只有更适合的.

但是有个问题,通道直方图计算匹配的时候贼慢贼慢的,我的电脑计算的时候执行了8秒多.

  1. def has_group_list(list_group, m2):
  2. ret = False
  3. if list_group:
  4. for klist in list_group:
  5. if m2 in klist:
  6. ret = True
  7. break
  8. return ret
  9.  
  10. # 通过得到每个通道的直方图来计算相似度
  11. def classify_hist_with_split(image1, image2, size=(256, 256)):
  12. # 将图像resize后,分离为三个通道,再计算每个通道的相似值
  13. image1 = cv2.resize(image1, size)
  14. image2 = cv2.resize(image2, size)
  15. sub_image1 = cv2.split(image1)
  16. sub_image2 = cv2.split(image2)
  17. sub_data = 0
  18. for im1, im2 in zip(sub_image1, sub_image2):
  19. sub_data += calculate(im1, im2)
  20. sub_data = sub_data / 3
  21. return sub_data
  22.  
  23. # 计算单通道的直方图的相似值
  24. def calculate(image1, image2):
  25. hist1 = cv2.calcHist([image1], [0], None, [256], [0.0, 255.0])
  26. hist2 = cv2.calcHist([image2], [0], None, [256], [0.0, 255.0])
  27. # 计算直方图的重合度
  28. degree = 0
  29. for i in range(len(hist1)):
  30. if hist1[i] != hist2[i]:
  31. degree = degree + (1 - abs(hist1[i] - hist2[i]) / max(hist1[i], hist2[i]))
  32. else:
  33. degree = degree + 1
  34. degree = degree / len(hist1)
  35. return degree
  36.  
  37. list_group = []
  38.  
  39. def get_list_group():
  40. tmpgroup = []
  41. tmpIJ = ''
  42. for i in range(1, 15):
  43. for j in range(1, 9):
  44. # ij 坐标1
  45. m1 = 'x' + str(i) + 'y' + str(j)
  46. for n in range(1, 15):
  47. for m in range(1, 9):
  48. # nm 坐标2
  49. m2 = 'x' + str(n) + 'y' + str(m)
  50. img1 = cv2.imread('openpic/' + m1 + ".png")
  51. img2 = cv2.imread('openpic/' + m2 + ".png")
  52. hn = classify_hist_with_split(img1, img2)
  53.  
  54. if hn > 0.80:
  55. if tmpIJ != m1:
  56. if tmpgroup:
  57. list_group.append(tmpgroup)
  58. tmpgroup = []
  59. print(tmpIJ)
  60. print(list_group)
  61. tmpIJ = m1
  62. else:
  63. if not has_group_list(list_group, m2):
  64. if m1 not in tmpgroup:
  65. tmpgroup.append(m1)
  66. tmpgroup.append(m2)
  67.  
  68. get_list_group()
  69. print(list_group)

打印list_group

  1. [['x1y1', 'x1y3', 'x5y6', 'x8y4', 'x13y8', 'x14y8'], ['x1y2', 'x1y8', 'x2y4', 'x3y8', 'x6y8', 'x7y2'], ['x1y4', 'x11y1'], ['x1y5', 'x4y7', 'x6y6', 'x11y3', 'x11y5', 'x12y8'], ['x1y6', 'x3y2', 'x4y8', 'x8y1', 'x9y7', 'x10y4', 'x11y4', 'x14y3'], ['x1y7', 'x2y1', 'x3y3', 'x4y3', 'x6y4', 'x8y5'], ['x2y2', 'x2y3', 'x7y8', 'x12y2', 'x13y1', 'x14y5'], ['x2y5', 'x6y3', 'x6y5', 'x8y8'], ['x2y6', 'x6y2'], ['x2y7', 'x5y5'], ['x2y8', 'x3y6', 'x12y5', 'x14y4'], ['x3y1', 'x5y1', 'x9y1', 'x10y3', 'x13y2', 'x13y5'], ['x3y4', 'x7y3', 'x7y4', 'x10y2', 'x11y8', 'x12y6', 'x13y4', 'x13y7', 'x14y2', 'x14y6'], ['x3y5', 'x4y4', 'x7y6', 'x9y8'], ['x3y7', 'x14y1'], ['x4y1', 'x9y2', 'x10y8', 'x11y2'], ['x4y2', 'x4y6', 'x12y3', 'x12y4'], ['x4y5', 'x5y7', 'x6y1', 'x9y5'], ['x5y2', 'x13y6'], ['x5y3', 'x7y1'], ['x5y4', 'x8y2'], ['x5y8', 'x9y3', 'x10y5', 'x11y7', 'x12y7', 'x13y3'], ['x6y7', 'x9y6'], ['x7y5', 'x8y3', 'x9y4', 'x10y7'], ['x7y7', 'x11y6'], ['x8y6', 'x12y1'], ['x8y7', 'x10y1'], ['x10y6', 'x14y7']]

问我为什么视频中没有等待8秒时间, 笑笑笑.....

看代码:

  1. """ list_group 写入文件 """
  2. fileObject = open('data_list_group.txt', 'w')
  3. ret = json.dump(link_points,fileObject)
  4. fileObject.close()
  5.  
  6. """ 读取 list_group """
  7. f= open('data_list_group.txt','r')
  8. list_group=[]
  9. for i in f.readlines():
  10. list_group.append(i.strip('\n'))
  11. print(list_group)

模拟鼠标点击

现在算法有了, 小方块坐标有了, 可联通的link_points列表有了,相似图片分组list_group有了,就差模拟鼠标点击了,pyautogui登场

PyAutoGUI 简介

画个方形我也会:

  1. import pyautogui
  2.  
  3. for i in range(3):
  4. pyautogui.moveTo(300, 300, duration=0.25)
  5. pyautogui.moveTo(400, 300, duration=0.25)
  6. pyautogui.moveTo(400, 400, duration=0.25)
  7. pyautogui.moveTo(300, 400, duration=0.25)

OK! 例子在手, 说走就走!

完整代码

config.py

  1. # -*- coding:utf-8 -*-
  2. """常量配置文件"""
  3.  
  4. PLAYER = 'gnash' # player
  5.  
  6. SWF_PATH = 'play.swf'
  7.  
  8. GAME_Width = 750 # flash 实际 750*450
  9.  
  10. GAME_HEIGHT = 450
  11.  
  12. PLAYER_JK = '-j' + str(GAME_Width) + ' -k' + str(GAME_HEIGHT) # jk 窗口宽高,参考 gnash -h
  13.  
  14. PLAYER_X = 100 # XY 窗口 距离(0.0)坐标的位置长度与高度
  15.  
  16. PLAYER_Y = 100 # XY 窗口 距离(0.0)坐标的位置长度与高度
  17.  
  18. GRID_SIZE = 40 # 网格大小
  19.  
  20. GRID_X_NUM = 14 # 网格X轴个数
  21.  
  22. GRID_Y_NUM = 8 # 网格Y轴个数
  23.  
  24. X11_HEIGHT = 30 # Ubuntu X11 窗体 title栏高度
  25.  
  26. PLAY_BTN = (315, 380) # player flash 开始游戏按钮坐标
  27.  
  28. CENTER_LEFT = 80 # 中心网格左侧距离
  29.  
  30. CENTER_TOP = 145 # 中心网格顶部高度距离
  31.  
  32. GRID_DIR = "img/" # 网格小方块目录
  33.  
  34. GRID_MIC_DIR = 'img_micro/' # 裁剪后小方块保存目录
  35.  
  36. GRID_ZOOM = 5 # 裁剪小方块 上 下 左 右 5px

play.swf

回到本页顶部,自己手动玩哈.

screenshot.py

  1. # -*- coding:utf-8 -*-
  2. import pyscreenshot as ImageGrab
  3. from PIL import Image
  4. import time, os
  5. from config import *
  6.  
  7. class screenshot_class:
  8. """
  9. 截取 player 窗体图片在init中
  10. """
  11.  
  12. def __init__(self):
  13. im = ImageGrab.grab([PLAYER_X, PLAYER_Y, PLAYER_X + GAME_Width,
  14. PLAYER_Y + GAME_HEIGHT + X11_HEIGHT]) # X1,Y1,X2,Y2 (X2-X1=750,Y2-Y1=450+30)
  15. im.save("1.png")
  16. print("截取player图片完成")
  17.  
  18. time.sleep(1)
  19. im = Image.open('1.png')
  20. im = im.crop((CENTER_LEFT, CENTER_TOP, GRID_X_NUM * GRID_SIZE + CENTER_LEFT,
  21. GRID_Y_NUM * GRID_SIZE + CENTER_TOP)) # 80,145,40*14+80,40*8+155
  22. im.save('2.png')
  23. print("裁剪截图处理完成")
  24.  
  25. def save_grid(self):
  26. """
  27. 保存小方块
  28. """
  29. print("cut cut cut .......")
  30. if not os.path.exists(GRID_DIR):
  31. os.mkdir(GRID_DIR) # 小方块存放目录
  32. if not os.path.exists(GRID_MIC_DIR):
  33. os.mkdir(GRID_MIC_DIR) # 裁剪后的小方块存放目录
  34. img = Image.open('2.png')
  35. for y in range(0, GRID_Y_NUM): # height高
  36. for x in range(0, GRID_X_NUM): # width长
  37. r = (x * GRID_SIZE, y * GRID_SIZE, GRID_SIZE + GRID_SIZE * x, GRID_SIZE + GRID_SIZE * y)
  38. crop_img = img.crop(r)
  39. x_play = x + 1
  40. y_play = abs(GRID_Y_NUM - y) # 原坐标是从左上开始计算Y轴,将其转换为从(0,0) 开始
  41. crop_img.save(GRID_DIR + "x" + str(x_play) + "y" + str(y_play) + '.png')
  42. crop_img = crop_img.crop((GRID_ZOOM, GRID_ZOOM, GRID_SIZE - GRID_ZOOM,
  43. GRID_SIZE - GRID_ZOOM)) # 生成的40*40 左右上下裁剪5px (5, 5, 35, 35)
  44. crop_img.save(GRID_MIC_DIR + "x" + str(x_play) + "y" + str(y_play) + '.png')
  45. time.sleep(1)
  46. print("cut 小方块完成")
  47.  
  48. if __name__ == '__main__':
  49. print("from img")
  50. screenshot_class()

autolinlink.py

  1. # -*- coding:utf-8 -*-
  2. import pyautogui
  3. from itertools import combinations
  4. import cv2
  5. import os, time
  6. from config import *
  7. import screenshot
  8.  
  9. class autolinlink:
  10. """autolinlink"""
  11.  
  12. @staticmethod
  13. def open_swf():
  14. """打开player"""
  15. os.popen(PLAYER + " " + SWF_PATH + " " + PLAYER_JK + " " + "-X" + str(PLAYER_X) + " -Y" + str(PLAYER_Y))
  16. print("打开player完成")
  17.  
  18. @staticmethod
  19. def play_btn():
  20. """play btn 开始按钮"""
  21. pyautogui.moveTo(PLAYER_X + PLAY_BTN[0], PLAYER_Y + PLAY_BTN[1], duration=3.6) # 移动鼠标至play位置
  22. pyautogui.click()
  23. print("已进入游戏")
  24.  
  25. def screen_shot(self):
  26. time.sleep(0.2) # 休眠0.2s 等待background后的网格小方块加载
  27. screenshot.screenshot_class()
  28. screenshot.screenshot_class.save_grid(self)
  29.  
  30. def get_points(self):
  31. """
  32. 获取 可联通点 坐标
  33. :return: list_points
  34. """
  35. print("获取可联通点坐标")
  36. link_points = [] # 可联通点坐标
  37. for x in range(0, GRID_X_NUM + 2): # 网格个数+2 可以认为网格小方块的外围有一圈空的方块
  38. for y in range(0, GRID_Y_NUM + 2): # 网格个数+2
  39. load_name = "x" + str(x) + "y" + str(y)
  40. if not os.path.exists(GRID_MIC_DIR + load_name + '.png'):
  41. link_points.append(load_name)
  42.  
  43. return link_points
  44.  
  45. def get_group(self):
  46. """
  47. 获取 相似图片group
  48. :return: list_group
  49. """
  50. print("计算图片相似度,大概持续8秒左右.....")
  51. list_group = []
  52.  
  53. # f = open('data_list_group.txt')
  54. # list_group = json.load(f)
  55. # return list_group
  56.  
  57. tmp_group = [] # 每个相同小方块的group分组
  58. tmpIJ = '' # 每个相同坐标分组中,第一个坐标1,之后循环与该点匹配判断是否在同一个分组
  59. for i in range(1, GRID_X_NUM + 1): # GRID_X_NUM+1 [1-14]区间
  60. for j in range(1, GRID_Y_NUM + 1):
  61. # ij 坐标1
  62. m1 = 'x' + str(i) + 'y' + str(j)
  63. for n in range(1, GRID_X_NUM + 1):
  64. for m in range(1, GRID_Y_NUM + 1):
  65. # nm 坐标2
  66. m2 = 'x' + str(n) + 'y' + str(m)
  67. img1 = cv2.imread("./" + GRID_MIC_DIR + m1 + ".png")
  68. img2 = cv2.imread(GRID_MIC_DIR + m2 + ".png")
  69. hn = self.classify_hist_with_split(img1, img2)
  70. if hn > 0.80: # 大于0.8认为小方块相同
  71. if tmpIJ != m1:
  72. if tmp_group:
  73. list_group.append(tmp_group)
  74. tmp_group = []
  75. tmpIJ = m1
  76. else:
  77. if not self.has_group_list(list_group, m2):
  78. if m1 not in tmp_group:
  79. tmp_group.append(m1)
  80. tmp_group.append(m2)
  81. return list_group
  82.  
  83. def classify_hist_with_split(self, image1, image2, size=(256, 256)):
  84. """通过得到每个通道的直方图来计算相似度"""
  85. # 将图像resize后,分离为三个通道,再计算每个通道的相似值
  86. image1 = cv2.resize(image1, size)
  87. image2 = cv2.resize(image2, size)
  88. sub_image1 = cv2.split(image1)
  89. sub_image2 = cv2.split(image2)
  90. sub_data = 0
  91. for im1, im2 in zip(sub_image1, sub_image2):
  92. sub_data += self.calculate(im1, im2)
  93. sub_data = sub_data / 3
  94. return sub_data
  95.  
  96. def calculate(self, image1, image2):
  97. """计算单通道的直方图的相似值"""
  98. hist1 = cv2.calcHist([image1], [0], None, [256], [0.0, 255.0])
  99. hist2 = cv2.calcHist([image2], [0], None, [256], [0.0, 255.0])
  100. # 计算直方图的重合度
  101. degree = 0
  102. for i in range(len(hist1)):
  103. if hist1[i] != hist2[i]:
  104. degree = degree + (1 - abs(hist1[i] - hist2[i]) / max(hist1[i], hist2[i]))
  105. else:
  106. degree = degree + 1
  107. degree = degree / len(hist1)
  108. return degree
  109.  
  110. def has_group_list(self, list_group, p):
  111. """判断当前点坐标在不在group中"""
  112. ret = False
  113. if list_group:
  114. for klist in list_group:
  115. if p in klist:
  116. ret = True
  117. break
  118. return ret
  119.  
  120. def run(self, list_group, link_points, tmp_points_list=[]):
  121. """执行操作"""
  122. total = (GRID_X_NUM + 2) * (GRID_Y_NUM + 2)
  123. sum_link_points = len(link_points)
  124. haslink = False
  125. if link_points != total:
  126. for k in list_group: # k=(x2y1,x2y4,x12y12,x90y90)
  127. for i in combinations(k, 2): # permutations 排列重复组合,combinations 组合不重复
  128. if i[0] not in link_points and i[1] not in link_points: # 排除 当前k () 已在link_points中
  129. llk = self.linlink(i, link_points)
  130. if llk:
  131. tmp_points_list.append(i[0])
  132. tmp_points_list.append(i[1])
  133.  
  134. if i == ('x5y4', 'x8y2'):
  135. time.sleep(1) # flash "加时"魔法干扰,临时sleep 1s
  136.  
  137. time.sleep(0.5)
  138. self.click(i[0], i[1])
  139. haslink = True
  140. break
  141.  
  142. if sum_link_points != total and haslink == False:
  143. print('剩余点无法消除')
  144. elif sum_link_points == total:
  145. print('完成')
  146. else:
  147. print('再来一次')
  148. list_group, link_points, tmp_points_list = self.update_group(list_group, link_points, tmp_points_list)
  149. return self.run(list_group, link_points, tmp_points_list)
  150.  
  151. def update_group(self, list_group, link_points, tmp_points_list):
  152. link_points += tmp_points_list
  153. # 删除list_group 已连接数据
  154. for i in list_group:
  155. for t in tmp_points_list:
  156. if t in i:
  157. i.remove(t)
  158. tmp_points_list = []
  159.  
  160. return list_group, link_points, tmp_points_list
  161.  
  162. def linlink(self, xy, link_points):
  163. ret = False
  164. # 判断点是否能联通
  165. x1 = xy[0].split('y')[0]
  166. y1 = 'y' + xy[0].split('y')[1]
  167. x2 = xy[1].split('y')[0]
  168. y2 = 'y' + xy[1].split('y')[1]
  169. result = self.lineCase(xy, link_points, x1, y1, x2, y2)
  170.  
  171. if result:
  172. print(xy, '直线连接可联通')
  173. ret = True
  174. else:
  175. result = self.onceCorner(xy, link_points, x1, y1, x2, y2) # 一次拐角
  176. if result:
  177. print(xy, '一次拐角可联通')
  178. ret = True
  179. else:
  180. result = self.doubleCorner(xy, link_points, x1, y1, x2, y2) # 两个拐角
  181. if result:
  182. print(xy, '两次拐角可联通')
  183. ret = True
  184. else:
  185. # print(xy, '两次拐角貌似不可联通')
  186. pass
  187. return ret
  188.  
  189. # 两次拐角
  190. def doubleCorner(self, xy, link_points, x1, y1, x2, y2):
  191. ret = False
  192. first_match = 1
  193. for px in range(GRID_X_NUM + 2): # (GRID_X_NUM + 2) * (GRID_Y_NUM + 2)
  194. for py in range(GRID_Y_NUM + 2):
  195. tmpx = 'x' + str(px)
  196. tmpy = 'y' + str(py)
  197. # 以第一个点坐标寻找拐点,
  198. if (tmpx == x1 and tmpy != y1) or (tmpy == y1 and tmpx != x1): # 第一个点坐标四个方向但除了自己
  199. # 任意拐点,(px,py)与第一个点坐标为直线情况,与第二个点为一次拐角情况
  200. lineC = self.lineCase(xy, link_points, x1, y1, tmpx, tmpy, corner=True)
  201. onceC = self.onceCorner(xy, link_points, tmpx, tmpy, x2, y2)
  202. if lineC and onceC and first_match == 1:
  203. first_match += 1
  204. ret = True
  205. break
  206.  
  207. return ret
  208.  
  209. # 一次拐角
  210. def onceCorner(self, xy, link_points, x1, y1, x2, y2):
  211. ret = False
  212. C = (x2, y1) # 用 第一个的Y,第二个X # C,D 为中间(p,Tmp) 的过渡点
  213. D = (x1, y2) # 用 第一个X,第二个Y
  214. # C点分别与第一个点 与第二点 联通
  215. ClineCaseY = self.lineCase(xy, link_points, x1, y1, C[0], C[1], True) # 即 (x1,y1,x2,y1)
  216. ClineCaseX = self.lineCase(xy, link_points, x2, y2, C[0], C[1], True) # 即 (x2,y2,x2,y1)
  217. if ClineCaseX and ClineCaseY: # C点与第一个点的X轴联通,与第二个点的Y轴联通
  218. ret = True
  219. DlineCaseX = self.lineCase(xy, link_points, x1, y1, D[0], D[1], True) # 即 (x1,y1,x1,y2)
  220. DlineCaseY = self.lineCase(xy, link_points, x2, y2, D[0], D[1], True) # 即 (x2,y2,x1,y2)
  221. if DlineCaseX and DlineCaseY: # D点与第一个点的X轴联通,与第二个点的Y轴联通
  222. ret = True
  223.  
  224. return ret
  225.  
  226. def lineCase(self, xy, link_points, x1, y1, x2, y2, corner=False):
  227. ret = False
  228. if x1 == x2: # x纵轴 相同
  229. xmin = min(int(y1[1:]), int(y2[1:])) # 字符串不能进行max min比较 int(max(x1,x2)[1:]) # x1=9 x2=13
  230. xmax = max(int(y1[1:]), int(y2[1:]))
  231. point_num = xmax - xmin - 1
  232. if point_num == 0 and corner == False:
  233. ret = True
  234. elif point_num == 0 and corner: # 相连 and 来自拐角
  235. if x2 + y2 in link_points: # 判断后(xy,tmp) tmp中间点 点是否为空
  236. ret = True
  237. else:
  238.  
  239. point_num_able = 0
  240. for i in range(xmin + 1, xmax):
  241. if x1 + "y" + str(i) in link_points:
  242. point_num_able += 1
  243.  
  244. if point_num == point_num_able and point_num > 0 and ((x2 + y2 in link_points and corner) or corner == False): # 可联通点的个数等于同轴points点的个数
  245. ret = True
  246.  
  247. if y1 == y2:
  248.  
  249. ymin = min(int(x1[1:]), int(x2[1:])) # 字符串不能进行max min比较 int(max(x1,x2)[1:]) # x1=9 x2=13
  250. ymax = max(int(x1[1:]), int(x2[1:]))
  251. point_num = ymax - ymin - 1
  252.  
  253. if point_num == 0 and corner == False:
  254. ret = True
  255. elif point_num == 0 and corner:
  256. if x2 + y2 in link_points:
  257. ret = True
  258. else:
  259.  
  260. point_num_able = 0
  261. for i in range(ymin + 1, ymax):
  262. if "x" + str(i) + y1 in link_points:
  263. point_num_able += 1
  264.  
  265. if (point_num == point_num_able) and point_num > 0 and (( x2 + y2 in link_points and corner) or corner == False): # 可联通点的个数等于同轴points点的个数 # x2+y2 in link_points and corner作为拐角的点也必须为空
  266. ret = True
  267. return ret
  268.  
  269. def click(self, p1, p2):
  270. """模拟鼠标点击""" # ('x4y8', 'x8y1')
  271. # 设置x(0,0),y(0,0)坐标
  272. init_pint = (PLAYER_X + CENTER_LEFT - GRID_SIZE + 20, CENTER_TOP + X11_HEIGHT + PLAYER_Y + ((GRID_Y_NUM) * GRID_SIZE) - 20) # x=80-40 , y= 145+30+9*40 x+20 与y-20可以让鼠标移动到小方块的中心
  273.  
  274. # x轴 相加, Y轴 相减
  275. pX1 = init_pint[0] + int(p1.split('y')[0][1:]) * GRID_SIZE
  276. pY1 = init_pint[1] - int(p1.split('y')[1]) * GRID_SIZE
  277.  
  278. pX2 = init_pint[0] + int(p2.split('y')[0][1:]) * GRID_SIZE
  279. pY2 = init_pint[1] - int(p2.split('y')[1]) * GRID_SIZE
  280.  
  281. pyautogui.moveTo(pX1, pY1, duration=0.05)
  282. pyautogui.click()
  283.  
  284. pyautogui.moveTo(pX2, pY2, duration=0.05)
  285. pyautogui.click()
  286.  
  287. if __name__ == '__main__':
  288. app = autolinlink()
  289. app.open_swf()
  290. app.play_btn()
  291. app.screen_shot()
  292. link_points = app.get_points()
  293. list_group = app.get_group()
  294. if link_points and list_group:
  295. app.run(list_group, link_points)

源文件:

http://u.163.com/Bhfyp9nm   提取码: LkYikiOz

Python 实现auto linlink 连连看的更多相关文章

  1. python之auto鼠标/键盘事件

    mouse_key.py import os import time import win32gui import win32api import win32con from ctypes impor ...

  2. 用Python玩连连看是什么效果?

    1.前言 Python实现的qq连连看辅助, 仅用于学习, 请在练习模式下使用, 请不要拿去伤害玩家们... 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道 ...

  3. Python之RabbitMQ操作

    RabbitMQ是一个消息代理,从“生产者”接收消息并传递消息至“消费者”,期间可根据规则路由.缓存.持久化消息.“生产者”也即message发送者以下简称P,相对应的“消费者”乃message接收者 ...

  4. catkin_make broken after intalling python 3.5 with anaconda

    "No module named catkin_pkg.package" on catkin_make w/ Indigo I have the problem after ana ...

  5. Debian中如何切换默认Python版本

    当你安装 Debian Linux 时,安装过程有可能同时为你提供多个可用的 Python 版本,因此系统中会存在多个 Python 的可执行二进制文件,你可以按照以下方法使用 ls 命令来查看你的系 ...

  6. How to change from default to alternative Python version on Debian Linux

    https://linuxconfig.org/how-to-change-from-default-to-alternative-python-version-on-debian-linux You ...

  7. 更改Ubuntu默认python版本的方法

    当你安装 Debian Linux 时,安装过程有可能同时为你提供多个可用的 Python 版本,因此系统中会存在多个 Python 的可执行二进制文件.一般Ubuntu默认的Python版本都为2. ...

  8. 0005-20180422-自动化第六章-python基础学习笔记

    day6 内容回顾: 1. 变量 2. 条件 3. while循环 4. 数据类型 - int - bit_length - bool - True/False - str - upper - low ...

  9. Linux下python默认版本切换成替代版本

    本文链接自http://www.myhack58.com/Article/48/66/2016/71806.htm 当你安装 Debian Linux 时,安装过程有可能同时为你提供多个可用的 Pyt ...

随机推荐

  1. 最近公共祖先(LCT)

    来一发\(LCT\)求\(LCA\) \(LCT\)在时间上不占据优势,码量似乎还比树剖,倍增,\(Tarjan\)大一点 但是却是一道\(LCT\)的练手题 对于每一个询问,我们只需要把其中一个点( ...

  2. PHP7 中 ?? 与? :的区别

    ??是PHP7版本的新特性,它与?:的区别在哪里呢 ?? $b = $a?? $c ;相当于$b= isset($a)?$a:$c; ?: $b = $a?$a: $c 则是 $b = !empty( ...

  3. python学习day16 模块(汇总)

    模块(总) 对于range py2,与py3的区别: py2:range() 在内存中立即把所有的值都创建,xrange() 不会再内存中立即创建,而是在循环时边环边创建. py3:range() 不 ...

  4. 给dataframe添加一列索引

    测试数据自己瞎编的 需求:给现在df数据添加一列sid,要求这一列是和stock一一对应的整数 代码如下: import pandas as pd test_data = {'stock': ['AA ...

  5. react16 渲染流程

    前言 react升级到16之后,架构发生了比较大的变化,现在不看,以后怕是看不懂了,react源码看起来也很麻烦,也有很多不理解的地方. 大体看了一下渲染过程. react16架构的变化 react ...

  6. C++ STL的一些操作

    priority_queue 最常用的当然是在dij的时候. #include <queue> struct node { int x, dis; bool operator < ( ...

  7. kubernetes 核心技术概念(二)之 volume namespace annoation

    volume k8s通过数据卷来提供pod数据的持久化,k8s的数据卷是对docker数据卷的扩展,k8s的数据卷是pod级别的,用来实现pod中容器的文件共享 volume是pod中能被多个容器访问 ...

  8. volatile&synchronized&diff

    1. 三大性质简介 在并发编程中分析线程安全的问题时往往需要切入点,那就是两大核心:JMM抽象内存模型以及happens-before规则(在这篇文章中已经经过了),三条性质:原子性,有序性和可见性. ...

  9. 自搭的一个系统框架,使用Spring boot+Vue+Element

    基于:jdk1.8.spring boot2.1.3.vue-cli3.4.1 特性:    ~ 数据库访问使用spring data jpa+alibaba druid    ~ 前后端数据交互使用 ...

  10. 转载skbbuf整理笔记

    1.http://blog.csdn.net/yuzhihui_no1/article/details/38666589 2.http://blog.csdn.net/yuzhihui_no1/art ...