[跳一跳] Nodejs + Opencv 版

2018-01-13 11:03:17来源:https://juejin.im/post/5a5581526fb9a01cbc6e461c作者:稀土掘金人点击

分享

游戏中,小人蓄力时长决定弹跳距离,成功跳到下一个墩子,即加分。


目标即获取小人位置,获取目标点位置然后计算距离。


在做的过程中,发现,人物弹跳方向为斜向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>

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中,不同通道数的图像,根据坐标获取图像的颜色信息,创建一个形状等,描述的都很清楚。


找顶点的方式,想到了使用漫水法填充背景色,然后二值化+反色取到最靠上的顶点。


实际过程中会遇到:


小人比新出现的墩子高,或者小人跳到中心出现的波纹和加分字体比新墩子高。
所以,加一步,用背景色覆盖小人及其上方部分。
墩子白色,或者浅绿色,与背景接近,使用OSTU二值化,效果不理想。 所以,加一步,
设定颜色范围为80~255,
如果有灰度值大于235(接近白色)的都直接变成80(底边界值)。 创建一个灰度化算法,与背景色在通道上差异较大者,远离背景灰度。通过buffer取10个像素RGB三个通道的平均背景色,然后每个元素与之做差求平方和。减少渐变影响,差在13以内,置为0。

然后,用此灰度图像,对背景进行漫水填充,闽值40,使用 BINARY_INV 方式,处理得到二值图。然后逐行搜索,找到顶点所在行。然后用数组方法,根据方差,对该行元素进行简易分类,得到最长连续像素范围,取中间值,即为顶点横坐标。


处理过程:
从frame中截取待处理区域



将小人用背景色覆盖
用背景色绘制矩形,覆盖小人。


黑色正方形为最终找到的顶点位置。
使用自定义的灰度方法,将图片增强灰度化



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



高斯模糊能简易去除噪点儿影响


二值化




图中最顶上一行,不规则。遇到顶点时,可能被消除。 所以取横坐标时,从最上一行向下数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



最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台