晓查 编译整理
量子位 出品 | 公众号 QbitAI

小时候的你在游戏中搓着手柄,在现实中是否也会模仿这《拳皇》的动作?用身体控制游戏角色的体感游戏很早就已出现,但需要体感手柄(Wii)或体感摄像头(微软Kinect)配合。而现在,笔记本就能帮你做到这一切!

最近,有一位名叫Minko Gechev的软件工程师实现了在笔记本上玩《真人快打》(Mortal Kombat),只需要一颗前置摄像头即可。

早在5年前,他就曾展示过体感玩格斗游戏的项目成果:

当时实现方案很简单,也没有利用时下流行的AI技术。但是这套算法离完美还相去甚远,因为需要单色画面背景作为参照,使用条件苛刻。

5年间,无论是网络浏览器的API,还是WebGL都有了长足的发展。于是这名工程师决定用TensorFlow.js来改进他的游戏程序,并在他个人Blog上放出了完整教程。

量子位对文章做了编译整理,主要内容是训练模型识别《真人快打》这款游戏主要有拳击、踢腿两种动作,并通过模型输出结果控制游戏人物做出对应动作。

以下就是他Blog的主要内容:

简介

我将分享用TensorFlow.js和MobileNet创建动作分类算法的一些经验,全文将分为以下几部分:

  • 为图片分类收集数据

  • 使用imgaug进行数据增强

  • 使用MobileNet迁移学习

  • 二元分类和N元分类

  • 在浏览器中使用TensorFlow.js模型训练图片分类

简单讨论使用LSTM进行动作分类

我们将开发一种监督深度学习模型,利用笔记本摄像头获取的图像来分辨用户是在出拳、出腿或者没有任何动作。最终演示效果如下图:

理解本文内容需要有基本的软件工程和JavaScript知识。如果你有一些基本的深度学习知识会很有帮助,但非硬性要求。

收集数据

深度学习模型的准确性在很大程度上取决于训练数据的质量。因此,我们首要的目标是建立一个丰富的训练数据集。

我们的模型需要识别人物的拳击和踢腿,所以应当从以下三个分类中收集图像:

  • 拳击

  • 踢腿

  • 其他

为了这个实验,我找到两位志愿者帮我收集图像。我们总共录制了5段视频,每段都包含2-4个拳击动作和2-4个踢腿动作。由于收集到的是视频文件,我们还需要使用ffmpeg将之转化为一帧一帧的图片:

ffmpeg -i video.mov $filename%03d.jpg

最终,在每个目录下,我们都收集了大约200张图片,如下:

注:除了拳击和踢腿外,图片目录中最多的是“其他”部分,主要是走动、转身、开关视频录制的一些画面。如果这部分内容太多,会有风险导致训练后的模型产生偏见,把应该归于前两类的图片划分到“其他”中,因此我们减少了这部分图片的量。

如果只使用这600张相同环境、相同人物的图片,我们将无法获得很高的准确度。为了进一步提高识别的准确度,我们将使用数据增强对样本进行扩充。

数据增强

数据增强是一种通过已有数据集合成新样本的技术,可以帮助我们增加数据集的样本量和多样性。我们可以将原始图片处理一下转变成新图,但处理过程不能太过激烈,好让机器能够对新图片正确归类。

常见的处理图片的方式有旋转、反转颜色、模糊等等。网上已有现成软件,我将使用一款由Python编写的imgaug的工具(项目地址见附录),我的数据增强代码如下:

np.random.seed(44)ia.seed(44)

def main():    for i in range(1, 191):        draw_single_sequential_images(str(i), "others", "others-aug")    for i in range(1, 191):        draw_single_sequential_images(str(i), "hits", "hits-aug")    for i in range(1, 191):        draw_single_sequential_images(str(i), "kicks", "kicks-aug")

def draw_single_sequential_images(filename, path, aug_path):    image = misc.imresize(ndimage.imread(path + "/" + filename + ".jpg"), (56, 100))    sometimes = lambda aug: iaa.Sometimes(0.5, aug)    seq = iaa.Sequential(        [            iaa.Fliplr(0.5), # horizontally flip 50% of all images            # crop images by -5% to 10% of their height/width            sometimes(iaa.CropAndPad(                percent=(-0.05, 0.1),                pad_mode=ia.ALL,                pad_cval=(0, 255)            )),            sometimes(iaa.Affine(                scale={"x": (0.8, 1.2), "y": (0.8, 1.2)}, # scale images to 80-120% of their size, individually per axis                translate_percent={"x": (-0.1, 0.1), "y": (-0.1, 0.1)}, # translate by -10 to +10 percent (per axis)                rotate=(-5, 5),                shear=(-5, 5), # shear by -5 to +5 degrees                order=[0, 1], # use nearest neighbour or bilinear interpolation (fast)                cval=(0, 255), # if mode is constant, use a cval between 0 and 255                mode=ia.ALL # use any of scikit-image's warping modes (see 2nd image from the top for examples)            )),            iaa.Grayscale(alpha=(0.0, 1.0)),            iaa.Invert(0.05, per_channel=False), # invert color channels            # execute 0 to 5 of the following (less important) augmenters per image            # don't execute all of them, as that would often be way too strong            iaa.SomeOf((0, 5),                [                    iaa.OneOf([                        iaa.GaussianBlur((0, 2.0)), # blur images with a sigma between 0 and 2.0                        iaa.AverageBlur(k=(2, 5)), # blur image using local means with kernel sizes between 2 and 5                        iaa.MedianBlur(k=(3, 5)), # blur image using local medians with kernel sizes between 3 and 5                    ]),                    iaa.Sharpen(alpha=(0, 1.0), lightness=(0.75, 1.5)), # sharpen images                    iaa.Emboss(alpha=(0, 1.0), strength=(0, 2.0)), # emboss images                    iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, 0.01*255), per_channel=0.5), # add gaussian noise to images                    iaa.Add((-10, 10), per_channel=0.5), # change brightness of images (by -10 to 10 of original value)                    iaa.AddToHueAndSaturation((-20, 20)), # change hue and saturation                    # either change the brightness of the whole image (sometimes                    # per channel) or change the brightness of subareas                    iaa.OneOf([                        iaa.Multiply((0.9, 1.1), per_channel=0.5),                        iaa.FrequencyNoiseAlpha(                            exponent=(-2, 0),                            first=iaa.Multiply((0.9, 1.1), per_channel=True),                            second=iaa.ContrastNormalization((0.9, 1.1))                        )                    ]),                    iaa.ContrastNormalization((0.5, 2.0), per_channel=0.5), # improve or worsen the contrast                ],                random_order=True            )        ],        random_order=True    )

    im = np.zeros((16, 56, 100, 3), dtype=np.uint8)    for c in range(0, 16):        im[c] = image

    for im in range(len(grid)):        misc.imsave(aug_path + "/" + filename + "_" + str(im) + ".jpg", grid[im])

每张图片最后都被扩展成16张照片,考虑到后面训练和评估时的运算量,我们减小了图片体积,每张图的分辨率都被压缩成100*56。

建立模型

现在,我们开始建立图片分类模型。处理图片使用的是CNN(卷积神经网络),CNN适合于图像识别、物体检测和分类领域。

迁移学习

迁移学习允许我们使用已被训练过网络。我们可以从任何一层获得输出,并把它作为新的神经网络的输入。这样,训练新创建的神经网络能达到更高的认知水平,并且能将源模型从未见过的图片进行正确地分类。

我们在文中将使用MobileNet神经网络(安装包地址见附录),它和VGG-16一样强大,但是体积更小,在浏览器中的载入时间更短。

在浏览器中运行模型

在这一部分,我们将训练一个二元分类模型。

首先,我们浏览器的游戏脚本MK.js中运行训练过的模型。代码如下:

const video = document.getElementById('cam');const Layer = 'global_average_pooling2d_1';const mobilenetInfer = m => (p): tf.Tensor<tf.Rank> => m.infer(p, Layer);const canvas = document.getElementById('canvas');const scale = document.getElementById('crop');

const ImageSize = {  Width: 100,  Height: 56};

navigator.mediaDevices  .getUserMedia({    video: true,    audio: false  })  .then(stream => {    video.srcObject = stream;  });

以上代码中一些变量和函数的注释:

  • video:页面中的HTML5视频元素

  • Layer:MobileNet层的名称,我们从中获得输出并把它作为我们模型的输入

  • mobilenetInfer:从MobileNet接受例子,并返回另一个函数。返回的函数接受输入,并从MobileNet特定层返回相关的输出

  • canvas:将取出的帧指向HTML5的画布

  • scale:压缩帧的画布

第二步,我们从摄像头获取视频流,作为视频元素的源。对获得的图像进行灰阶滤波,改变其内容:

const grayscale = (canvas: HTMLCanvasElement) => {  const imageData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);  const data = imageData.data;  for (let i = 0; i < data.length; i += 4) {    const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;    data[i] = avg;    data[i + 1] = avg;    data[i + 2] = avg;  }  canvas.getContext('2d').putImageData(imageData, 0, 0);};

第三步,把训练过的模型和游戏脚本MK.js连接起来。

let mobilenet: (p: any) => tf.Tensor<tf.Rank>;tf.loadModel('http://localhost:5000/model.json').then(model => {  mobileNet    .load()    .then((mn: any) => mobilenet = mobilenetInfer(mn))    .then(startInterval(mobilenet, model));});

在以上代码中,我们将MobileNet的输出传递给mobilenetInfer方法,从而获得了从网络的隐藏层中获得输出的快捷方式。此外,我还引用了startInterval。

const startInterval = (mobilenet, model) => () => {  setInterval(() => {    canvas.getContext('2d').drawImage(video, 0, 0);

    grayscale(scale      .getContext('2d')      .drawImage(        canvas, 0, 0, canvas.width,        canvas.width / (ImageSize.Width / ImageSize.Height),        0, 0, ImageSize.Width, ImageSize.Height      ));

    const [punching] = Array.from((      model.predict(mobilenet(tf.fromPixels(scale))) as tf.Tensor1D)    .dataSync() as Float32Array);

    const detect = (window as any).Detect;    if (punching >= 0.4) detect && detect.onPunch();

  }, 100);};

startInterval正是关键所在,它每间隔100ms引用一个匿名函数。在这个匿名函数中,我们把视频当前帧放入画布中,然后压缩成100*56的图片后,再用于灰阶滤波器。

在下一步中,我们把压缩后的帧传递给MobileNet,之后我们将输出传递给训练过的模型,通过dataSync方法返回一个一维张量punching。

最后,我们通过punching来确定拳击的概率是否高于0.4,如果是,将调用onPunch方法,现在我们可以控制一种动作了:

用N元分类识别拳击和踢腿

在这部分,我们将介绍一个更智能的模型:使用神经网络分辨三种动作:拳击、踢腿和站立。

const punches = require('fs')  .readdirSync(Punches)  .filter(f => f.endsWith('.jpg'))  .map(f => `${Punches}/${f}`);

const kicks = require('fs')  .readdirSync(Kicks)  .filter(f => f.endsWith('.jpg'))  .map(f => `${Kicks}/${f}`);

const others = require('fs')  .readdirSync(Others)  .filter(f => f.endsWith('.jpg'))  .map(f => `${Others}/${f}`);

const ys = tf.tensor2d(  new Array(punches.length)    .fill([1, 0, 0])    .concat(new Array(kicks.length).fill([0, 1, 0]))    .concat(new Array(others.length).fill([0, 0, 1])),  [punches.length + kicks.length + others.length, 3]);

const xs: tf.Tensor2D = tf.stack(  punches    .map((path: string) => mobileNet(readInput(path)))    .concat(kicks.map((path: string) => mobileNet(readInput(path))))    .concat(others.map((path: string) => mobileNet(readInput(path))))) as tf.Tensor2D;

我们对压缩和灰阶化的图片调用MobileNet,之后将输出传递给训练过的模型。 该模型返回一维张量,我们用dataSync将其转换为一个数组。 下一步,通过使用Array.from我们将类型化数组转换为JavaScript数组,数组中包含我们提取帧中三种姿势的概率。

如果既不是踢腿也不是拳击的姿势的概率高于0.4,我们将返回站立不动。 否则,如果显示高于0.32的概率拳击,我们会向MK.js发出拳击指令。 如果踢腿的概率超过0.32,那么我们发出一个踢腿动作。

以下就是完整的演示效果:

动作识别

如果我们收集到更大的多样性数据集,那么我们搭建的模型就能更精确处理每一帧。但这样就够了吗?显然不是,请看以下两张图:

它们都是踢腿动作,但实际上在视频中有很大的不同,是两种不同的动作。

为了识别动作,我们还需要使用RNN(循环神经网络),RNN的优势在处理时间序列问题,比如

  • 自然语言处理,词语的意思需要联系上下文

  • 根据历史记录,预测用户将要访问的页面

  • 识别一系列帧中的动作

若要识别动作,我们还需要将数帧画面输入CNN,再将输出结果输入RNN。

总结

在本文中,我们开发了一个图像分类模型。为此,我们手动提取视频帧并收集数据集,将它们分成三个不同的类别,然后使用imgaug进行数据增强。

之后,我们通过MobileNet来解释什么是迁移学习,以及我们如何利用MobileNet。经过训练,我们的模型达到了90%以上的准确率!

为了在浏览器中使用我们开发的模型,我们将它与MobileNet一起加载,并从用户的相机中每100ms取出一帧,识别用户的动作,并使用模型的输出来控制《真人快打3》中的角色。

最后,我们简单讨论了如何通过RNN来进一步改进我们的模型。

我希望你们能够和我一样喜欢这个小项目。

附录:

原文地址:
https://blog.mgechev.com/2018/10/20/transfer-learning-tensorflow-js-data-augmentation-mobile-net/

原动作识别项目地址:
https://github.com/mgechev/movement.js

JS版《真人快打》项目地址:
https://github.com/mgechev/mk.js

imgaug:
https://github.com/aleju/imgaug

MobileNet神经网络:
https://www.npmjs.com/package/@tensorflow-models/mobilenet

加入社群

量子位AI社群开始招募啦,欢迎对AI感兴趣的同学,在量子位公众号(QbitAI)对话界面回复关键字“交流群”,获取入群方式;

此外,量子位专业细分群(自动驾驶、CV、NLP、机器学习等)正在招募,面向正在从事相关领域的工程师及研究人员。

进专业群请在量子位公众号(QbitAI)对话界面回复关键字“专业群”,获取入群方式。(专业群审核较严,敬请谅解)

活动策划招聘

量子位正在招聘活动策划,将负责不同领域维度的线上线下相关活动策划、执行。欢迎聪明靠谱的小伙伴加入,并希望你能有一些活动策划或运营的相关经验。相关细节,请在量子位公众号(QbitAI)对话界面,回复“招聘”两个字。

量子位 QbitAI · 头条号签约作者

վ'ᴗ' ի 追踪AI技术和产品新动态

有笔记本就能玩的体感游戏!TensorFlow.js实现体感格斗教程相关推荐

  1. aiwi国内最大体感游戏平台 领跑体感游戏第一线

    AIWI是全球首款将你的iPhone/iPod Touch当作游戏手柄来控制计算机游戏的软件.你只需将你的计算机和iPhone/iPod Touch透过蓝芽或无线网络联机,你就可以开始体验令人兴奋的体 ...

  2. (图)不可错过的好看好玩的射箭体感游戏

    ,aiwi滑雪 玩上手之后,可以直接挑战美女指导员,可别小看美女指导员,体感游戏手机,AIWI体感游戏平台上的所有游戏都很适合女生玩,女生玩起体感游戏一点也不输给男生喔! 在现场活动部分更有美女指导员 ...

  3. 在线试玩,在体感游戏中打败泰森,这位小哥破解了任天堂「拳无虚发」

    视学算法报道 编辑:张倩.陈萍 加入体感控制,这位小哥破解了原版任天堂的拳击格斗游戏「拳无虚发」. 在经典红白机上玩拳击游戏是很多人的童年回忆,其中就包括任天堂 1987 年发布的拳击格斗游戏--「P ...

  4. 春节假期和幺儿一起玩xbox360的体感游戏

    很多年前,我在商场就看到过体感游戏,大大电视,面前有几个小孩,玩得很熟练,看起来蛮有意思. 当时我在想,为什么个体商家愿意拿出设备摆摊,难道这里面有很丰厚的利润? 今年我终于有一个想法,在春节假期和幺 ...

  5. 【体感游戏】没有iPhone也能玩AIWI

    现在全球都在玩AIWI体感游戏了,可是没有iPhone也没有iPod怎么办?就不能玩aiwi了吗? 别担心, AIWI 也针对没有智能型手机的消费者,贴心的研发设计了AIWI 专属的游戏手柄,让更多的 ...

  6. 【你知道么】90后们玩过体感游戏吗?

    90后最牛逼的AIWI体感游戏你玩了没?aiwi序列号童鞋们别再错过机会啦,体感游戏领导厂商AIWI在CHINAJOY(第九届中国国际数码互动娱乐展)上发表了市面上最犀利的体感手机系统AIWI APP ...

  7. 【DIY娱乐】手机链接PC玩体感游戏

    手机链接PC玩体感游戏,你要准备以下内容: 1:家里台式主机一台 2:智能手机一台 3:家中路由带无线网络wifi功能 4:下载和安装aiwi体感游戏手机和PC客户端 aiwi电脑客户端下载地址:ht ...

  8. Scratch也能玩体感游戏

    Scratch也能玩体感游戏 上一次我们利用图像分类技术,让机器学会了识别石头剪刀布(如果没明白说啥请看上一帖)等三种手势,如果大家有试验的话,可能会发现识别率并不是太高,会有误判,问题主要在于我们给 ...

  9. 用腾讯优图AI视觉模组在树莓派上玩吃火锅体感游戏

    今年上半年,由于疫情原因,Switch 大火,Switch 平台上有许多款体感游戏,比如:健 身环大冒险.马力欧赛车.舞力全开.这些游戏凭着"硬核健身"的标签吸引了大量玩家, 通过 ...

最新文章

  1. 如何实现一个元素居中
  2. ibatis解决sql注入问题 .
  3. SQL Server 2005与2000写法上的差别
  4. 转:ibatis动态sql
  5. 产品报价单模板_一文说透报价单,这么做才是专业!附模板及注意事项
  6. 于变局中开新局!《2021中国SaaS市场研究报告》报告发布
  7. oracle12c 不添加host,Oracle12c部署,允许远程访问-Go语言中文社区
  8. [LeetCode]15. 3Sum
  9. 一起来啃书——PHP看书
  10. mysql免安装_腾讯云Ubuntu18.04部置Django2系列(二):Ubuntu18.04 安装Mysql
  11. CSS3 必须要知道的10 个顶级命令
  12. 计算机专业排名2017教育部,软件工程专业大学排名最新版(教育部2017学科排名数据整理)...
  13. phpmyadmin java_phpMyAdmin的配置
  14. mongoose报错Cast to ObjectId failed for value
  15. 测试在团队中有啥用?
  16. es的插件 ik分词器的安装和使用
  17. Arp病毒(motou.exe,smss.com,smss.exe)解决方案
  18. 金蝶K3WISE13.1销售发票不能删除
  19. H264/AVC SEI和VUI
  20. 怎样配置外汇ea服务器运行,外汇EA如何在MT4上运行?以及如何挂到服务器?

热门文章

  1. Python入门之爬虫--自动下载图片
  2. Java版工程项目管理系统平台+企业工程系统源码+助力工程企业实现数字化管理
  3. etc 微信小程序
  4. [34期] KO战队0812笔记总结|房子
  5. python脚本执行错误_crontab常见错误(执行python脚本 no module)
  6. 分享ThinVirt云桌面软件实现NVIDIA P40显卡为虚拟机分配vGPU过程
  7. 做什么都要形成自己的库,这样测试起来就能一气呵成
  8. 科创人·微软中国CTO韦青:数智时代创业得跳下巨人肩膀
  9. ant design vue treeDefaultExpandAll 更换数据后没有自动展开
  10. ug896-vivado-ip中文文档 | Xilinx