[跳一跳] Nodejs + Opencv 版

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

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

最終效果

內容

  • 使用的開箱即用工具java

  • 遊戲目標分析node

  • 設備數據(藉助別人github repo,非ADB)python

    • 手機屏幕圖像獲取,同屏顯示
    • 手機觸摸事件發送
  • 圖像處理webpack

  • 技能點:git

    • 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模板識別,直接可以準確搜索到人頭位置。 因此遊戲目標再簡化爲:github

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

設備數據

手機屏幕圖像獲取,同屏顯示

openstf/minicap,openstf/minitouch部署到安卓設備,而後經過adb啓動socket,再經過adb鏈接socket,後續請求與發送數據不須要再次建立adb鏈接,實時性較好。 web

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. 高斯模糊+漫水填充背景。


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

  1. 二值化
    圖中最頂上一行,不規則。遇到頂點時,可能被消除。 因此取橫座標時,從最上一行向下數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方式。

不足與總結

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

不足

  • 中心位置跳偏,沒有作修正。
  • webpack 掌握欠缺,未配置 koa 熱部署
  • 使用圖像處理取得頂點位置花費的時間,彷佛比將每一個墩子頂面截圖使用templateMatch方法還要長。
  • 缺乏代碼組織套路,代碼可讀性待提升。

總結

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

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

相關文章
相關標籤/搜索