王者体验展 – 线下运营辅助技术的创新和实践
7月26日,“探秘王者世界”——《王者荣耀》大型浸入式实景体验展在上海杨浦滨江船厂顺利开启。
这个利用实景搭建技术与光影技术相融合所打造而成的、首个以《王者荣耀》为主题的线下体验展,是目前全球规模领先的独立IP体验展,历时两年重金打造,光筹备就经过了50次的创意方向更新,超过100次的人物模型监修以及200多稿场景设计方案的推敲,更有国内众多顶级团队倾力合作。
为帮助广大召唤师更好地了解和体验展区内容,早在6月份的时候,王者的授权团队联系到我们,我们也因此得到了一个任务:
「基于用户侧,给巡回展制定一个可行的运营辅助方案」
同时在兼顾良好用户体验前提下,尽可能提供一些创新技术点,从而最终实现用户可以从线下到线上的创新体验。经过几轮策划,我们最终决定为用户提供一个承载了展会信息和互动的小程序,也定出了最终的交互稿。然后在一个月内的时间,我们从前端到后台,为需求方实现了这样的小程序:
该小程序包括以下两大类功能:
- 基础功能:签到、场景互动、照片墙和发放优惠券/CDKEY
- 创新功能:AI识别旗帜发现彩蛋功能,室内定位导览功能
在小程序云开发出现之前,类似的功能需要专门的后台开发人员来协助实现,但有了小程序云开发,一些基础功能可以直接通过云开发来完成,这部分的工作在以往的需求过程中已经进行了多次,且小程序云开发简单易用,对此感兴趣的同学可以查看官方文档的相关API即可,本文不展开来细讲。
本文主要阐述另外两个我们觉得比较有意思的技术点,同时会就其作用,发散一下未来可发展的应用方向:
- 基于 TensorFlow.js 技术,实现 AI图像识别 ,用于“AI识别旗帜发现彩蛋”功能;
- 基于 iBeacon 技术,实现现 室内定位 ,用于“室内定位导览功能”功能。
AI图像识别
我们基于TensorFlow.js实现了AI识图功能,用于“长城守卫军场馆”。用户进入场馆后在小程序内特定界面可触发识图功能,用户将手机相机对准特定的道具后,利用TensorFlow.js判断用户是否对准了正确的道具,如果是,则显示相关彩蛋。
TensorFlow简介
TensorFlow(以下简称为TF)是一个谷歌大脑团队开发的开源软件库,用于各种感知和语言理解任务的机器学习。
通过创建和训练模型,TF可以完成很多事情,从基础的图像识别、语音识别,到复杂的机器翻译、AR、邮件自动回复,摄像头动作捕捉等等,都可以通过它来实现。
简单来说,就是你先通过精密的算法和大量的训练(训练模型),教会TF来做一些事情,它学会以后,就可以通过你之前的训练(加载模型),来完成一些复杂的操作或计算。
技术落地
简单介绍了TF,我们回到王者体验展小程序这个项目本身,王者体验展是我们首次演练TensorFlow技术的落地点,项目的需求是用户在小程序中调用手机摄像头,根据摄像头拍摄到的画面进行实时识别,如果画面中出现了特定的LOGO,则出现小彩蛋。
对这个需求进行技术拆分,其核心技术有两点,获取摄像头的实时画面帧,将获取到的实时帧利用TFJS进行图像识别。如果识别到的画面当中出现了LOGO,则显示彩蛋,否则给予一定的提示。
通过摄像头控制吃豆人
类似效果大家可以查看这个TFJS的官方DEMO:https://teachablemachine.withgoogle.com/,这个DEMO可以让用户通过不同的4个画面来控制吃豆人的行进方向,比如你可以让摄像头对准你自己的手,你的手摆出不同的造型,吃豆人前往不同的方向。
具体原理是摄像头拍摄4不同的多组画面,训练出对应的模型后,再次对准这4个不同的画面,TFJS就可以识别到你对准的到底是哪一个,从而控制吃豆人来前往不同的方向。
换个思路,在我们这个项目中,是期望识别到一个带有LOGO的“守卫军”旗帜,那么我同样训练4个图像,其中1个是“守卫军”旗帜,其他3个不是,当用户识别时,如果返回的结果是旗帜,则判定正确。这样就能达到我们的需求目的了。
梳理一下流程,这个需求的具体流程如上所示,分为用户端和管理员端,双端的核心交叉点在于训练出来的“模型”,管理端训练好具体的模型并上传到服务器,而用户在具体做图片识别时,获取到实时帧,然后TFJS利用管理端训练好的模型来进行识别和判断。
具体效果大家可以看一下当时现场录制的一个演示视频:
这里我们主要讲一下“实时帧”的获取,以及TFJS的使用,还有模型的使用和训练,至于权限获取之类的,相信大家也很熟悉了。
实时帧
项目的落地离不开底层的支持,刚好在5月9号小程序基础库的2.7.0当中,camera组件开始支持实时帧的获取,开发者可以通过camera组件获取到摄像头捕捉到的每一帧数据,然后对数据进行处理。
https://developers.weixin.qq.com/miniprogram/dev/framework/release/
因为是小程序自身的组件和API,使用起来很简单,WXML中添加camera组件,然后在JS代码中通过onCameraFrame来监听实时帧即可。
WXML:
1 |
<span class="hljs-tag"><<span class="hljs-name">camera</span> <span class="hljs-attr">device-position</span>=<span class="hljs-string">"back"</span> <span class="hljs-attr">flash</span>=<span class="hljs-string">"off"</span> <span class="hljs-attr">frame-size</span>=<span class="hljs-string">"medium"</span>></span><span class="hljs-tag"></<span class="hljs-name">camera</span>></span> |
JavaScript:
1 2 3 4 5 6 7 |
<span class="hljs-keyword">let</span> context = wx.createCameraContext() <span class="hljs-keyword">let</span> listener = context.onCameraFrame(<span class="hljs-function">(<span class="hljs-params">frame</span>) =></span> {<span class="hljs-comment">//监听实时帧</span> <span class="hljs-keyword">this</span>.classify(frame.data, {<span class="hljs-comment">//将监听到的实时帧传递给classify进行判断</span> width: frame.width, height: frame.height }); }) |
TFJS
TensorFlow有一个专门的JavaScript版本,一般简称TFJS。不过它依赖很多传统的WebAPI,而小程序对这些的API支持是另外一套方案,所以要在小程序中使用TFJS就要对一些API进行修改。
之前有一些开发者自己动手进行了一些TFJS方面的移植,但在4月19日,小程序的插件中心中有了对小程序支持的TFJS版本,对小程序的兼容性方面有了极大的提升。
https://mp.weixin.qq.com/wxopen/pluginbasicprofile?action=intro&appid=wx6afed118d9e81df9
相对实时帧的获取,TFJS的使用要相对麻烦一些,因为步骤较多,下面分为三大块来讲,分别是配置、模型使用、模型训练。
配置TFJS
1. 在插件页面点击“添加插件”,将插件添加到自己的小程序中。
2. 在小程序的app.json中声明插件:
1 2 3 4 5 6 7 8 9 10 |
{ ... <span class="hljs-string">"plugins"</span>: { <span class="hljs-string">"tfjsPlugin"</span>: { <span class="hljs-string">"version"</span>: <span class="hljs-string">"0.0.6"</span>, <span class="hljs-string">"provider"</span>: <span class="hljs-string">"wx6afed118d9e81df9"</span> } } ... } |
3. 在自己小程序项目的根目录下新建package.json,在其中声明需要使用的TFJS(1.2.2是老版本了,现在有新版本),并通过npm来安装,如果使用过node.js的同学,这个过程应该不陌生:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ <span class="hljs-string">"name"</span>: <span class="hljs-string">"miniprogram"</span>, <span class="hljs-string">"version"</span>: <span class="hljs-string">"1.0.0"</span>, ............ ............ ............ ............ <span class="hljs-string">"dependencies"</span>: { <span class="hljs-string">"@tensorflow/tfjs-core"</span>: <span class="hljs-string">"1.2.2"</span>, <span class="hljs-string">"@tensorflow/tfjs-layers"</span>: <span class="hljs-string">"1.2.2"</span>, <span class="hljs-string">"fetch-wechat"</span>: <span class="hljs-string">"0.0.3"</span> } } |
4. 通过微信开发工具的构建功能来将通过npm安装的TFJS构建为微信可用的版本,具体构建菜单在微信开发者工具中的“工具 – 构建 npm”。
5. TFJS构建出来的一些JS文件比较大,超过500KB;而微信开发者工具在编译上传的时候,超过500KB的JS文件处于性能考虑,不混淆和压缩;同时,小程序包的整体大小又不能超过2MB。所以,这里如果不压缩就整体超过2MB,而开发者工具自身又不去压缩,我们就只能自己动手了,通过一些在线压缩工具,把超过500KB的JS文件手动压缩一下,然后覆盖,保证小程序整体小于2MB即可。
使用模型
经过上面的引入和配置后,我们在具体的页面JS中引入和配置TFJS包。
1 2 3 4 5 6 7 8 9 10 11 12 |
<span class="hljs-keyword">const</span> fetchWechat = <span class="hljs-keyword">require</span>(<span class="hljs-string">"fetch-wechat"</span>); <span class="hljs-keyword">const</span> plugin = requirePlugin(<span class="hljs-string">"tfjsPlugin"</span>); <span class="hljs-keyword">const</span> tf = <span class="hljs-keyword">require</span>(<span class="hljs-string">"@tensorflow/tfjs-core"</span>); <span class="hljs-keyword">const</span> layers = <span class="hljs-keyword">require</span>(<span class="hljs-string">"@tensorflow/tfjs-layers"</span>); plugin.configPlugin({ <span class="hljs-comment">// polyfill fetch function</span> fetchFunc: fetchWechat.fetchFunc(), <span class="hljs-comment">// inject tfjs runtime</span> tf, <span class="hljs-comment">// provide webgl canvas</span> canvas: wx.createOffscreenCanvas() }); |
然后就是具体的模型加载和图像识别了,这里主要有3个部分,基础模型载入(loadTruncatedMobileNet)、自己训练的特定图像识别模型的载入(loadMyModel)、图像识别函数(classify)。
上面实时帧获取API每次获取到的画面数据,就是传递给了classify这个函数来进行识别,而这个函数又依赖基础模型和我们自己训练的模型。
classify函数会返回4个结果中的其中一个,如果识别到你期望的结果,做一下判断即可。
(示例代码,无法直接使用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
</code><code class="hljs javascript"><span class="hljs-keyword">let</span></code><code class="hljs javascript"> truncatedMobileNet,model; onLoad:<span class="hljs-function"><span class="hljs-keyword">function</span>()</span>{ <span class="hljs-keyword">this</span>.loadMyModel(); }, <span class="hljs-keyword">async</span> loadTruncatedMobileNet() {<span class="hljs-comment">//基础模型</span> <span class="hljs-comment">//基础训练模型</span> <span class="hljs-keyword">const</span> mobilenet = <span class="hljs-keyword">await</span> layers.loadLayersModel( <span class="hljs-string">"........../base_model/model.json"</span> ); <span class="hljs-keyword">const</span> layer = mobilenet.getLayer(<span class="hljs-string">"conv_pw_13_relu"</span>); <span class="hljs-keyword">return</span> layers.model({ <span class="hljs-attr">inputs</span>: mobilenet.inputs, <span class="hljs-attr">outputs</span>: layer.output }); }, <span class="hljs-keyword">async</span> loadMyModel() {<span class="hljs-comment">//自己训练的模型,识别特定图片</span> truncatedMobileNet = <span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.loadTruncatedMobileNet(); model = <span class="hljs-keyword">await</span> layers.loadLayersModel( <span class="hljs-string">"........../my-model.json"</span><span class="hljs-comment">//这里是现场训练好的模型地址</span> ); }, <span class="hljs-keyword">async</span> classify(ab, size) {<span class="hljs-comment">//图像识别</span> <span class="hljs-keyword">let</span> data = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Uint8Array</span>(ab); <span class="hljs-keyword">let</span> PREPROCESS_DIVISOR = tf.scalar(<span class="hljs-number">255</span> / <span class="hljs-number">2</span>); <span class="hljs-keyword">const</span> predictedClass = tf.tidy(<span class="hljs-function"><span class="hljs-keyword">function</span>() </span>{ <span class="hljs-keyword">let</span> temp = tf.browser.fromPixels( <span class="hljs-built_in">Object</span>.assign( { <span class="hljs-attr">data</span>: data }, size ), <span class="hljs-number">4</span> ); <span class="hljs-keyword">let</span> pixels = temp .slice([<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>], [<span class="hljs-number">-1</span>, <span class="hljs-number">-1</span>, <span class="hljs-number">3</span>]) .resizeBilinear([<span class="hljs-number">224</span>, <span class="hljs-number">224</span>]); <span class="hljs-keyword">let</span> preprocessedInput = tf.div( tf.sub(pixels.asType(<span class="hljs-string">"float32"</span>), PREPROCESS_DIVISOR), PREPROCESS_DIVISOR ); <span class="hljs-keyword">let</span> reshapedInput = preprocessedInput.reshape( [<span class="hljs-number">1</span>].concat(preprocessedInput.shape) ); <span class="hljs-keyword">const</span> embeddings = truncatedMobileNet.predict(reshapedInput); <span class="hljs-keyword">const</span> predictions = model.predict(embeddings); <span class="hljs-keyword">return</span> predictions.as1D().argMax(); }); <span class="hljs-keyword">const</span> classId = (<span class="hljs-keyword">await</span> predictedClass.data())[<span class="hljs-number">0</span>]; <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'图像识别结果:'</span>,classId); }, |
训练模型
这个项目中需要识别的画面是“守卫军”旗帜,而这面旗帜所在的场景在我们小程序开发的过程中一直都在布景和变化,直到我们出差到上海时,场景才基本布置完毕,甚至现场都还有一些细微调整。
所以我们需要一个可以通过手机在线训练模型的页面,并可以导出模型上传到线上。
文章开头的DEMO中已经具有了训练模型的功能,但是训练好的模型无法导出,我们可以对它稍加改造。
play改为了下载
因为我们只需要训练模型的功能,无需游玩,所以模型训练部分无需改动,只需要把它的“play”改为下载模型功能即可。
TFJS有一个.save方法(https://js.tensorflow.org/api/latest/#tf.LayersModel.save),可以保存训练好的模型,具体使用可以参照已经改动完毕的页面:https://pvp.qq.com/act/a20190727tyz/cross-domain/xunlian/index.html
用Chrome打开训练页面,将摄像头对准4个不同的画面,点击“Add Sample”不断拍摄4组画面,然后点击“训练”按钮训练好模型后,此时点击“下载”,浏览器会下载训练好的2个模型文件(json和bin),上传到线上,然后在上文说到的“loadMyModel”函数中修改成对应地址,就可以应用你的模型了。
技术展望
上面就是这个项目中图像识别的完整应用了,两个核心:
- Camera实时帧
- TensorFlow.js(TFJS)
这两个核心功能点可以做的事情,其实远不止这些,比如实时帧的获取方面,我们可以拿到摄像头具体的每一帧画面数据以后,将画面渲染到Canvas上,再在摄像头实景画面的基础上运用图像识别+3D模型叠加,那么就形成了游戏人物和显示场景的AR结合,比如我们这边最近入职的毕业生totorohuang就已经开始了3D+实景的尝试。
而TensorFlow本身则是一个机器学习系统,不仅可以运行在客户端,还可以运行在服务器端。我们这里用到的是基础的图像识别功能,它还可以做非常多其他机器方面的事情,比如人体动作识别。
brucewan大佬在前端将TFJS的人体动作识和3D游戏人物结合起来,让玩家可以通过上传一段自己的舞蹈视频,通过系统识别后,会将玩家的舞蹈动作套用到3D游戏人物中进行复现,让玩家心仪的游戏角色可以跟随自己的舞蹈。相对于之前运行在后端的动作识别系统,前端实现可以基于用户更快的反馈,无需等待服务器运算以及回传数据的等待时间。
室内定位
我们基于iBeacon.js实现了室内定位功能,用于小程序中最重要的,也是交互最多的“地图”界面。用户进入场馆后,小程序会根据布置在场馆内各处的iBeacon进行位置判定,从而达到室内定位的目的。同时,也可以根据定位衍生出自动激活小程序,自动发放CDK等功能。
iBeacon简介
iBeacon是一种低功耗信号传送器,我们的手机在开启蓝牙的情况下,就可以检测到它不断发送的信号。
通常情况下,GPS信号在室内定位不够准确,而iBeacon则可以让用户在室内以“米”为单位精确定位。搭配小程序的相关API,我们可以利用iBeacon方便的进行一些创意实现。
它大概长这个样子,一颗纽扣电池可以供电好几年:
技术落地
在王者体验展这个项目中,iBeacon主要承担两个功能:
- 地图导览;
- 小程序无感激活。
巡回展的场馆较大,用户进入场馆后有很大可能性不知道自己所处的位置,以及前方还有多少个景区可以游览,每个景区都有些什么玩法,于是我们通过小程序地图+iBeacon结合的方式,让玩家可以精确知道自己所处场馆。
因为iBeacon的信号范围有限,且信号容易受到各类室内物品的阻挡,我们在每一个场馆都布置了多个iBeacon,然后通过小程序不断轮询,根据当前监听到的iBeacon设备的远近距离,判断用户当前在哪一个场馆。再根据不同场馆的互动点,呈现给玩家不同的界面展示以及功能触发。具体流程如下:
布置iBeacon
在代码开始之前,我们需要在场馆布置(双面胶)iBeacon,布置遵循几个原则:
- 隐蔽;
- 密集;
- 遮蔽物少;
- 场馆与场馆之间留一些“空白”。
“隐蔽”很好理解,就算设备再美丽,除非为了场馆特制,否则都会和场景有所冲突,显得突兀。而且也会有游客不小心碰掉,甚至恶意损坏和带走,最终导致功能不可用。
“密集”是为了信号的稳定,以及冗余。如果某场馆内的单个设备失效,还有其他设备仍在发射信号。
“遮蔽物少”,因为iBeacon信号非常容易受到物体的干扰,所以尽量黏贴在屋顶这样的高位(如果是那种几十米的屋顶就算了……),一来遮挡较少,二来很少有用户抬头网上看。如果屋顶不方便,那就尽量选择那种只有单个物体的背后,做到隐蔽就好,不要放到那种成堆的物品后面,会容易导致更多干扰。
“留白”是为了让场馆和场馆之间少一些干扰,用户在A和B场馆之间的时候,难免有一些位置跳动,我们可以通过代码做一些优化处理,但适当的在场馆和场馆之间少一些,甚至不放iBeacon,会更好。
记录设备
每一个iBeacon设备都有uuid、major、minor这3个参数,我们可以对它们进行读取和修改。为了避免我们自己的iBeacon设备和其他人的冲突,我们一般都会个性化这3个参数。
比如这次我们就将所有uuid设为一致,然后major设为700(大涨!!!),然后minor设置一个不重复的编号。这里建议minor编号写在设备上,也方便后续看到设备就知道编号。
最后,我们将放置在每一个场馆的设备minor记录下来写到数据库里,方便后续用户端监听iBeacon时进行比对。比如A场馆对应minor为:561、562、563、564这4个iBeacon。
核心代码
下面是小程序蓝牙的开启接口,开启则调用wx.startBeaconDiscovery开始监听iBeacon,没开启就基于用户友好提示,让用户开启蓝牙。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
wx.openBluetoothAdapter({<span class="hljs-comment">//打开蓝牙</span> success: <span class="hljs-function"><span class="hljs-params">()</span> =></span> { <span class="hljs-comment">//如果打开成功,开始监听iBeacon</span> }, fail: <span class="hljs-function"><span class="hljs-params">()</span> =></span> { <span class="hljs-comment">//打开失败,则提示用户要打开蓝牙</span> } }); wx.onBluetoothAdapterStateChange(<span class="hljs-function"><span class="hljs-params">res</span> =></span> {<span class="hljs-comment">//监听蓝牙状态</span> <span class="hljs-keyword">if</span> (res.available) { <span class="hljs-comment">//如果蓝牙已经开启,开始监听iBeacon</span> } <span class="hljs-keyword">else</span> { <span class="hljs-comment">//如果蓝牙没有开启,则提示用户要打开蓝牙</span></code><code class="hljs typescript"> } });</code><code class="hljs typescript"> |
下面是iBeacon监听接口,需要注意的是,在iOS下返回的距离(accuracy)有时候会有负数。
当我们在wx.onBeaconUpdate中获取到了具体监听到的iBeacon列表后,我们需要对major进行判断,是不是700,minor是不是在我们的列表中,如果是,则代表这个iBeacon是我们的设备。
然后再进行距离(accuracy)排序,看看最近的1到2个iBeacon在数据库中对应的是哪个场馆,我们就知道用户具体所处位置了。知道了位置,后续的功能触发,就是具体不同业务,不同代码啦~
比如说到了A场馆,弹出提示,介绍场馆并展示互动小游戏。
比如让小程序成为场馆内的专用小程序,在场馆外无法适用,通过iBeacon判断用户在场馆内,自动激活小程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
wx.startBeaconDiscovery({<span class="hljs-comment">//开始监听iBeacon</span> uuids: [<span class="hljs-string">"B9317F30-F5F6-456E-AFF4-23336B57FE5D"</span>], <span class="hljs-attr">ignoreBluetoothAvailable</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">success</span>: <span class="hljs-function"><span class="hljs-params">()</span> =></span> { wx.onBeaconUpdate(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">res</span>) </span>{ <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i < res.beacons.length; i++) { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'major'</span>,res.beacons[i][<span class="hljs-string">"major"</span>]); <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'minor'</span>,res.beacons[i][<span class="hljs-string">"minor"</span>]); <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'距离'</span>,res.beacons[i][<span class="hljs-string">"accuracy"</span>]); } }); } }); |
技术展望
和TFJS一样,iBeacon也不仅仅是可以做场馆定位和无感激活。比如我们可以结合iBeacon做场馆内的人流热力图,在现场大屏幕展示当前最热门的场馆。
2019年TGC也用了ibeacon技术
此外还能做一些线下寻宝功能,在不同场馆随机出现“宝藏”,用户到达位置后使用“摇一摇”功能,开启“宝藏”。
等等等……
尾声
相对于之前,最近几年各类线下+线上的互动案例越来越多,除了上面讲到的2个技术点,我们之前在其他项目中还应用过智能印章、真人3D换脸、人体识别等等一系列技术,这些技术储备就像是一块一块的拼图,通过不同的创意搭配,可以拼合成不同的“形状”。
比如智能印章可以应用在小程序激活或者签到一类的服务,人体识别可以应用在场馆内的互动小游戏。
技术研究
原文地址:https://tgideas.qq.com/gicp/news/475/8505850.html?from=list