[跳一跳] Nodejs + Opencv 版

Songlairui發表於2019-03-04

赤裸裸的來蹭下熱點。 微信跳一跳小遊戲,風格簡約,忍不住動心思自動跳一跳。程式碼閱讀起來太費勁,決定寫一篇文章描述一下自己的程式碼。

僅供練習nodejs技能,勿討論作弊手段。

最終效果

內容

  • 使用的開箱即用工具

  • 遊戲目標分析

  • 裝置資料(藉助別人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模板識別,直接能夠準確搜尋到人頭位置。 所以遊戲目標再簡化為:

  1. 求彈跳的時間距離曲線。
  2. 求小人座標。
  3. 求頂點座標。

裝置資料

手機螢幕影象獲取,同屏顯示

openstf/minicap,openstf/minitouch部署到安卓裝置,然後通過adb啟動socket,再通過adb連線socket,後續請求與傳送資料不需要再次建立adb連線,實時性較好。

openstf 工具使用示意圖

啟動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>
複製程式碼

MirrorScreen.vue#L584

...
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中,不同通道數的影象,根據座標獲取影象的顏色資訊,建立一個形狀等,描述的都很清楚。

找頂點的方式,想到了使用漫水法填充背景色,然後二值化+反色取到最靠上的頂點。
實際過程中會遇到:

  1. 小人比新出現的墩子高,或者小人跳到中心出現的波紋和加分字型比新墩子高。
    所以,加一步,用背景色覆蓋小人及其上方部分。
  2. 墩子白色,或者淺綠色,與背景接近,使用OSTU二值化,效果不理想。 所以,加一步,
    設定顏色範圍為80~255,
    如果有灰度值大於235(接近白色)的都直接變成80(底邊界值)。 建立一個灰度化演算法,與背景色在通道上差異較大者,遠離背景灰度。通過buffer取10個畫素RGB三個通道的平均背景色,然後每個元素與之做差求平方和。減少漸變影響,差在13以內,置為0。

然後,用此灰度影象,對背景進行漫水填充,閩值40,使用 BINARY_INV 方式,處理得到二值圖。然後逐行搜尋,找到頂點所在行。然後用陣列方法,根據方差,對該行元素進行簡易分類,得到最長連續畫素範圍,取中間值,即為頂點橫座標。

處理過程:

  1. 從frame中擷取待處理區域
    原圖
  2. 將小人用背景色覆蓋
    用背景色繪製矩形,覆蓋小人。
    背景色覆蓋小人

    黑色正方形為最終找到的頂點位置。
  3. 使用自定義的灰度方法,將圖片增強灰度化
    灰度化

    grayExt2.js#L8
  4. 高斯模糊+漫水填充背景。

[跳一跳] Nodejs + Opencv 版

高斯模糊能簡易去除噪點兒影響

  1. 二值化
    [跳一跳] Nodejs + Opencv 版
    圖中最頂上一行,不規則。遇到頂點時,可能被消除。 所以取橫座標時,從最上一行向下數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。每次取小球位置太準確了,以至於沒有寫異常捕捉。

[跳一跳] Nodejs + Opencv 版

其他技術點

  • 使用 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方式。

不足與總結

這個輔助應用,是自己把所瞭解的技能連續堆積完成的,比demo大了。
此工具完全非開箱即用: electron 部分opencv部分

不足

  • 中心位置跳偏,沒有做修正。
  • webpack 掌握欠缺,未配置 koa 熱部署
  • 使用影象處理取得頂點位置花費的時間,似乎比將每個墩子頂面截圖使用templateMatch方法還要長。
  • 缺少程式碼組織套路,程式碼可讀性待提高。

總結

熟練了socket的使用、buffer的操作,熟悉了opencv的基本使用、vue directives的使用。嘗試了使用python。

最後。
實時性效果,坊一個以前的沒有opencv的自動極速變色龍的視訊:
youtu.be/7YSpqiYZJ0w

[跳一跳] Nodejs + Opencv 版

相關文章