赤裸裸的來蹭下熱點。
微信跳一跳小遊戲,風格簡約,忍不住動心思自動跳一跳。程式碼閱讀起來太費勁,決定寫一篇文章描述一下自己的程式碼。
僅供練習nodejs技能,勿討論作弊手段。
最終效果
- 自動蓄力彈跳
輔助線跳一跳:player.youku.com/embed/XMzMw…
自動跳一跳:player.youku.com/embed/XMzMw…
內容
-
使用的開箱即用工具
-
遊戲目標分析
-
裝置資料(藉助別人github repo,非ADB)
- 手機螢幕影像獲取,同屏顯示
- 手機觸控事件傳送
-
影像處理
-
技能點:
- Electron-vue
- Vue directives
- Promise、 async/await
- Nodejs Socket
- koa + websocket
- Opencv4nodejs
使用的開箱即用工具
- Opencv4Nodejs nodejs 呼叫 opencv 庫
- openstf/minicap socket方式安卓裝置螢幕截圖影像流。Android 5.0 以上,stream輸出幀率與裝置一致。
- openstf/minitouch 安卓裝置 sendevent 替代者,實時性高。
- electron-vue 使用electron直接與socket互動,並使用vue顯示螢幕。
遊戲目標分析
遊戲中,小人蓄力時長決定彈跳距離,成功跳到下一個墩子,即加分。
目標即獲取小人位置,獲取目標點位置然後計算距離。
在做的過程中,發現,人物彈跳方向為斜向30度,未跳到中心點的情況下,偏移位置似乎不會導致遊戲失敗。
於是遊戲目標簡化為搜尋小人位置,與搜尋墩子中心點橫座標。
墩子中心點橫座標,與墩子頂點橫座標基本一致,只有一個長方形墩子不一致。
小人的圓形頭部影像不變,使用opencv模板識別,直接能夠準確搜尋到人頭位置。
所以遊戲目標再簡化為:
- 求彈跳的時間距離曲線。
- 求小人座標。
- 求頂點座標。
裝置資料
手機螢幕影像獲取,同屏顯示
將openstf/minicap
,openstf/minitouch
部署到安卓裝置,然後通過adb啟動socket,再通過adb連線socket,後續請求與傳送資料不需要再次建立adb連線,實時性較好。
啟動Socket : /src/renderer/util/adbkit.js#L77 async function startMinicap
:
...
let command = util.format(
`LD_LIBRARY_PATH=%s exec %s %s`,
path.dirname(`/data/local/tmp/minicap.so`),
`/data/local/tmp/minicap`,
`-P 1080x1920@360x640/${orientation} -S -Q ${quality}`
)
// `-P 540x960@360x640/${orientation} -S -Q ${quality}`
status.tryingStart = true
let stdout = await client.shell(device.id, command)
...
複製程式碼
stdout 為標準輸出的socket物件,後續加一個200ms內無錯誤即resolve的Promise,令startMinicap可正確await。
連線Socket,獲取Stream : /src/renderer/util/getStream.js#L6 async function liveStream
:
...
var { err, stream } = await client
.openLocal(device.id, `localabstract:minicap`)
.timeout(10000)
.then(out => ({ stream: out }))
.catch(err => ({ err }))
...
複製程式碼
獲取stream ,然後使用on readable 事件取螢幕每幀圖片,格式為jpeg壓縮。
...
stream.on(`readable`, tryRead)
...
複製程式碼
function tryRead #L50,其邏輯為解析stream每次讀取到的buffer,按條件拼成jpeg raw buffer 。
此處可簡單做限影像重新整理頻率處理 #L154
Vue 中使用 canvas 顯示buffer影像
顯示影像,可以方便的反饋判別結果。
上一步的socket,可以在electron中輕鬆import,並可以方便的將每一個framebuffer 賦值給 vm.screendata 。
使用vue監聽screendata,即可實時將screendata顯示到canvas中。
這裡用到 vue 的 directives 。
<canvas v-screen=`screendata` id=`screen` :width="canvasWidth" :height="canvasHeight" :style="canvasStyle"></canvas>
複製程式碼
...
directives: {
screen(el, binding, vNode) {
// console.info(`[canvas Screen]`)
if (!binding.value) return
// console.info(`render an image ---- `, +new Date())
let BLANK_IMG = `data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==`
var g = el.getContext(`2d`)
var blob = new Blob([binding.value], { type: `image/jpeg` })
var URL = window.URL || window.webkitURL
var img = new Image()
img.onload = () => {
vNode.context.canvasWidth = img.width
vNode.context.canvasHeight = img.height
g.drawImage(img, 0, 0)
// firstImgLoad = true
img.onload = null
img.src = BLANK_IMG
img = null
u = null
blob = null
}
var u = URL.createObjectURL(blob)
img.src = u
},
...
}
...
複製程式碼
使用 URL.createObjectURL
為img生成一個src地址,然後將img畫到canvas中。
定義 directives
時,vNode
需要手動傳入,不能直接用this
。
【
此處,假裝一個動態GIF:
stream.on(`readable`,function tryRead(){
...
framedata = chunk.read()
callback(framedata)
...
})
function callback (framedata){
vm.screendata = framedata
}
每一個framedata 賦給 vm.screendata, Canvas上顯示的影像重新整理一下。
】
複製程式碼
程式碼中同樣使用directives
做了一個輔助線層,用來顯示輔助線,以及找到的點。
裝置觸控事件傳送
按照螢幕stream的方式,取得minitouch的socket,對socket按照minitouch README中格式進行write,即可完成觸控事件的模擬。
觸控時長的控制,通過控制touchdown與touchup的時間長度調節。相容裝置觸控事件,設定每超過200ms,進行原地touchmove一下。程式碼MirrorScreen.vue#L221
時間調節,通過async / await 實現。標準的api應用,似乎沒什麼可說的。
敲下地面
到此,準備好的工具,能夠提供給我截圖,畫點,精確ms時長蓄力,於是我採集到了一些資料:
X = [0,50,100,150,200,250,300,700,1000]
Y = [0,33, 69, 90,144,177,207,516, 753]
複製程式碼
得到方程式,準確度非極致,但能夠使用了。
f(x) = -6.232e-08 x^3 + 0.0001559 x^2 + 0.6601 x - 0.7638
複製程式碼
影像處理
首先, open4nodejs 的使用。 opencv4nodejs 的README講得挺全的。
最開始搜尋node版opencv時,發現有2.4版本有3.0版本。這個repo使用的3.0版本,安裝起來也很順利。
README中,不同通道數的影像,根據座標獲取影像的顏色資訊,建立一個形狀等,描述的都很清楚。
找頂點的方式,想到了使用漫水法填充背景色,然後二值化+反色取到最靠上的頂點。
實際過程中會遇到:
- 小人比新出現的墩子高,或者小人跳到中心出現的波紋和加分字型比新墩子高。
所以,加一步,用背景色覆蓋小人及其上方部分。 - 墩子白色,或者淺綠色,與背景接近,使用OSTU二值化,效果不理想。
所以,加一步,
設定顏色範圍為80~255,
如果有灰度值大於235(接近白色)的都直接變成80(底邊界值)。
建立一個灰度化演算法,與背景色在通道上差異較大者,遠離背景灰度。通過buffer取10個畫素RGB三個通道的平均背景色,然後每個元素與之做差求平方和。減少漸變影響,差在13以內,置為0。
然後,用此灰度影像,對背景進行漫水填充,閩值40,使用 BINARY_INV 方式,處理得到二值圖。然後逐行搜尋,找到頂點所在行。然後用陣列方法,根據方差,對該行元素進行簡易分類,得到最長連續畫素範圍,取中間值,即為頂點橫座標。
處理過程:
- 從frame中擷取待處理區域
- 將小人用背景色覆蓋
用背景色繪製矩形,覆蓋小人。黑色正方形為最終找到的頂點位置。
- 使用自定義的灰度方法,將圖片增強灰度化
- 高斯模糊+漫水填充背景。
高斯模糊能簡易去除噪點兒影響
- 二值化
圖中最頂上一行,不規則。遇到頂點時,可能被消除。
所以取橫座標時,從最上一行向下數n=3行,來計算。得到結果如前邊影像所示。有偏差,但在可接受範圍內。
同樣方式可以識別小藥瓶:
識別小人的位置
使用opencv的templateMatch方法,可快速得到結果 findTarget2.js#L11:
...
let ballMat = cv.imread(path.resolve(__dirname, `..`, `ball.jpg`), 0) # 小人頭部為固定圖片
...
let { maxLoc: ballPoint } = colorMat
.bgrToGray()
.matchTemplate(ballMat, 3)
.minMaxLoc()
...
複製程式碼
結果中取maxLoc即可得到小人底座位置存入變數ballPoint
。每次取小球位置太準確了,以至於沒有寫異常捕捉。
其他技術點
- 使用 electron-vue 建立直接與socket互動的應用,並對外提供socket,用來獲取當前影像。
- 使用 koa + vue,建立一個手動分析當前影像的web介面。opencv在此server中。
- 圖片分析,取最大連續分類的演算法: findTopXY.js#L24~L56 使用了陣列方法,對當前行元素進行了簡單的分類。
- electron-vue 每次除錯會重新整理,容易造成多次啟動安卓二進位制檔案造成adb卡死,遂將部分邏輯放在外部server中。server間互動使用socket。這裡使用
new Promise(r=>{cachedArray.push(r)}).then(...)
的方式,變種使用promise,完成socket返回資料之後繼續執行程式碼邏輯。實現先蓄力,然後 n 毫秒之後返回處理結果,再判定彈跳時間。
TODO
- [ ] 整理server,使用此輔助完全非開箱即用。
含有buffer內容的資料傳輸,改為flatbuffer方式。- [x] 簡易 demo 已完成: testFlatBuffer
不足與總結
這個輔助應用,是自己把所瞭解的技能連續堆積完成的,比demo大了。
此工具完全非開箱即用: electron 部分、opencv部分。
不足
- 中心位置跳偏,沒有做修正。
- webpack 掌握欠缺,未配置 koa 熱部署
- 使用影像處理取得頂點位置花費的時間,似乎比將每個墩子頂面截圖使用templateMatch方法還要長。
- 缺少程式碼組織套路,程式碼可讀性待提高。
總結
熟練了socket的使用、buffer的操作,熟悉了opencv的基本使用、vue directives的使用。嘗試了使用python。
最後。
實時性效果,坊一個以前的沒有opencv的自動極速變色龍的視訊:
youtu.be/7YSpqiYZJ0w