程式設計師玩連連看的正確姿勢

小蟲巨蟹發表於2018-05-30

too young too simple

一、絕地反擊

最近女票迷上了某平臺的連連看對戰小遊戲,於是免不了要找哥哥我 PK 一翻,雖然是被迫捲入戰爭,但是以朕驚世駭俗的智商,那當然是勝券在握啦~~ 沒曾想,幾個回合下來,竟被啪啪啪打臉,快把這個月的口糧都輸光了(每把5塊錢啊,肉疼!!) 哎,完全拼手速是沒有希望的了,得想辦法讓連連看自動打

“連連看”都不會打的直男們,趕緊去懟一局

二、可行性

前段時間跳一跳火起來的時候,有人就通過 adb 截圖併傳送到電腦分析,再求得距離然後計算出按鍵時長,最後通過 adb shell 自動按鍵,從而獲得完美跳跳分,這一招用在連連看是否管用呢? 理論上,靠譜,分解如下:

  1. adb 截圖傳到電腦
  2. 將連連看的點選區域識別為一個二維矩陣,每一種小動物用一個數字表示
  3. 對二維矩陣求解,計算出每個位置的點選順序陣列
  4. 通過 adb shell 一把梭,一次性點掉所有

醬紫如果順利的話並且不被女票發現,贏回三個月的口糧都很有希望呀~~

三、實施步驟

技術選型

從上一節的分析來看,方案的實施涉及到很多圖片的分析處理,Python 可以方便的呼叫很多圖片庫,而且網上也有很多作業可以抄,所以選擇基於 Python 來做

環境搭建

沒有很具體的安裝步驟,需要的諮詢谷歌哥

1) 安裝 adb 環境。安裝完成後,用資料線連線一臺 android 手機,執行一些簡單的 adb 命令預熱下

// 是否連線上
adb devices

// 可否截圖儲存
adb shell /system/bin/screencap -p /sdcard/screenshot.png
adb pull /sdcard/screenshot.png /yourDocuments/screenshot.png

// 可否點選螢幕
adb shell input tap 100 100
複製程式碼

2)安裝 python 和相關的圖片庫,在安裝 openCv 的時候還踩了個大坑,記錄了下,僅供參考

圖片處理

1)截圖儲存 在終端,執行如上的兩個 adb 命令就可以截圖儲存了,也就是說,這裡需要一個可以呼叫終端命令,同時可以等待返回的 Python 方法:

// 執行終端命令的方法
def sh(command):
    p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    print p.stdout.read()

// 截圖儲存
sh('adb shell /system/bin/screencap -p /sdcard/screenshot.png')
sh('adb pull /sdcard/screenshot.png /yourDocuments/screenshot.png')
複製程式碼

2)裁剪有效區域、再等比切出小動物頭像 如果不要求很通用只是對你的手機有效的話,那麼只需要將第一步截下來的螢幕用工具來量一量(如 Mark Man),就可以用如下方式裁剪出有效區域

from PIL import Image
def cut (im, x, y, w, h, name):
  region = im.crop((x, y, x+w, y+h))
  region.save("./screenshot/" + name + ".png")

# 有效點選區域裁剪 (不通用的做法是,把這個矩形的座標量出來)
gx = 43
gy = 401
gw = 993
gh = 1420
cut(Image.open("./screenshot/screenshot.png"), gx, gy, gw, gh, 'main')
複製程式碼

有效區域

如果要做得通用一些,就需要計算圖片的比例了(只用於打敗女票的,完全沒必要嘛)

然後再按照10行7列切成小塊,並且根據二維陣列的下標命名

# -*-coding:utf-8-*-
from PIL import Image
import cutImg
def cut ():
  im = Image.open("./screenshot/main.png")
  # 圖片的寬度和高度
  img_size = im.size
  width = img_size[0]
  height = img_size[1]
  distanceW = width / 7
  distanceH = height / 10
  print(distanceW, distanceH)
  x = 0
  y = 0
  for num in range(0, 10):
    for i in range(0, 7):
      x = distanceW * i
      y = distanceH * num
      name = str(num) + str(i)
      cutImg.cut(im, x + 15, y + 15, distanceW - 20, distanceH - 20, name)
  return [distanceW, distanceH]
複製程式碼

小動物頭像
3)解析小動物頭像輸出數字二維矩陣(第一回合)

這一步著實需要下功夫,還踩了不少坑~~

首先想到的是通過求解圖片的 hash 值,利用 hash 值來比對圖片的相似度(例如感知 hash 演算法)。網上有各種求 hash 值的演算法,實現起來倒也簡單,但是,比較的正確率只能達到百分之七、八十(這樣我們分析出的點選路徑,肯定打不過啦!!),主要是這些小動物頭像在 hash 演算法下顯得都太相似了,拿感知雜湊演算法來說: a) 縮小圖片尺寸 b) 轉為灰度圖片 c) 計算灰度平均值 d) 比較畫素的灰度 e) 計算雜湊值 f) 對比圖片指紋

小豬頭

小猴頭
想象一下,上面的小豬頭和小猴頭經過如上的變換後,還有多少差異呢?

轉念一想,這個問題在機器學習領域,不過是那種最最簡單的分類問題,so,完全可以先訓練一個模型出來

4)解析小動物頭像輸出數字二維矩陣(第一回合) Turicreate 是蘋果開源的基於 python 機器學習框架,特點是輕量(只是分類相似的圖片而已,當然是越簡單越好),先安裝之

然後將上面寫好的截圖裁剪程式碼多執行幾次,手工分類,準備好訓練資料:

分類儲存
0
給每種小動物建立一個資料夾,再將所有該種類的動物裝進去

開始訓練,並儲存模型:

#!/usr/bin/env python
#encoding=utf-8
import turicreate as tc
img_folder = 'data'
// 匯入資料
data = tc.image_analysis.load_images(img_folder, with_path=True)
// 使用檔名來做標籤
data['label'] = data['path'].apply(lambda path: path.split('/')[len(path.split('/')) - 2])
data.save('doraemon-walle.sframe')
// 百分之八十的資料用於訓練,百分之二十用於測試
train_data, test_data = data.random_split(0.8, seed=2)
// 開始訓練模型
model = tc.image_classifier.create(train_data, target='label')
// 測試模型
predictions = model.predict(test_data)
metrics = model.evaluate(test_data)
// 輸出測試結果
print(metrics['accuracy'])
model.save('my_model_file')
複製程式碼

執行到倒數第二行的時候,順利輸出1.0(百分百的正確率有木有):

正確率100%

使用訓練好的模型,輸出二維矩陣:

import turicreate as tc
loaded_model = tc.load_model('my_model_file')
def getDataset():
  data = tc.image_analysis.load_images('screenshot', with_path=True)
  arr = loaded_model.predict(data)
  result = []
  temp = []
  for index in range(len(arr)):
    if (index % 7 == 0):
      temp = []
    if ((index + 1) % 7 == 0):
      result.append(temp)
    // f 為 0,標記為未刪除
    temp.append({'v': int(arr[index]), 'f': 0})
  return result
複製程式碼

路徑求解

1)判斷兩個動物圖示可連 需要滿足如下條件: a) 相同的圖示 b) 兩種直接存在一條通路,它是一條只經過沒有圖案的地方、且轉折點不超過2個的折線 具體程式碼實現可以看看這篇博文的分析(雖然是 C 版),這裡我就不貼了,繁瑣佔篇幅

2) 搜尋路徑,最簡單粗暴的一種做法 (1)從矩陣中挑出一個未被標記為刪除的元素,(2)再從矩陣中餘下的不被標記刪除的元素尋找一個跟它一樣的元素,判斷是否可以相連,是則將兩個元素標記為刪除,並將點選座標壓入座標陣列,否則重複(2),(3)重複(1),知道找到所有的點選座標點 但是這種做法是 O(nXn),很遺憾,暫時也沒有想到更好的辦法,只是想到了一個小小的優化策略,開始先遍歷一輪,將所有挨著的相同圖示消掉(顯而易見的事情當然要先辦啦),減小 N,節省一下演算法的時間

然後在“盲狙”的過程中,因為迴圈停止的條件是找到所有的座標點,假如遊戲給了個無解的矩陣,或者我們們圖片識別錯了導致無解,就會陷入死迴圈(雖然這樣的概率極低,沒遇到過),所以要做一下迴圈保護

# 遍歷消除(盲狙)
def commonBuild():
  global data
  global pos
  for num in range(0, 10):
      for i in range(0, 7):
        item = data[num][i]
        if (item['f'] == 1):
          continue
        for ix in range(0, 10):
          if (item['f'] == 1):
            break
          for iy in range(0, 7):
            item1 = data[ix][iy]
            if (item1['f'] == 1 or item1['v'] != item['v'] or (ix == num and iy == i)):
              continue
            if (remove.canRemove(num, i, ix, iy, data) == 1):
              item['f'] = 1
              item1['f'] = 1
              pos.append(getPos(i, num))
              pos.append(getPos(iy, ix))
              break
// 達到 70 也即所有的座標都找到即停止
// 否則也最多迴圈十次
count = 0
while (len(pos) < 70 and count < 10):
  count = count + 1
  commonBuild()
print(count)
複製程式碼

很幸運,經過優化後的演算法,基本上每次 count 都輸出為 1,不需要遍歷太多次。假如真的出現了無解矩陣,迴圈了 10 次退出了,那該如何是好呢?這個時候自己將機器沒有打完的點掉也應該沒有難度了

adb 一把梭

克服艱難險阻把座標陣列計算出來之後,後面的事情就簡單了,執行 adb 命令一把梭

for index in range(len(pos)):
  command = 'adb shell input tap ' + str(pos[index][0]) + ' ' + str(pos[index][1])
  print(command)
  os.system(command)
複製程式碼

四、後來,我贏了麼?

然而並沒有!!! 因為每條 adb 命令的執行間隔基本差不多要到 1 秒,逐條執行完之後黃花菜都涼了,要知道正常人打完一局也就 30、40 秒,作為機器人,這打完居然要 1 分多鐘,真是弱智機器人

嘗試將命令寫入一個 sh 檔案,然後通過 adb shell 執行批處理檔案,稍微快了一點點,但是依然還需要幾十秒(在此之前還嘗試寫一個堡壘 app 來一次性接收座標,然後再 android 系統中執行命令,都木有用)。然後優化分析演算法的動力都木有了

不過話說回來,跟女票打遊戲,還要用贏的麼?

感興趣加微勾搭:facemagic2014

相關文章