量子位 出品 | 公众年夜众号 QbitAI

小时候的你在游戏中搓动手柄,在现实中是否也会模拟这《拳皇》的动作?用身体掌握游戏角色的体感游戏很早就已涌现,但须要体感手柄(Wii)或体感摄像头(微软Kinect)合营。
而现在,条记本就能帮你做到这统统!

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

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

有笔记本就能玩体感游戏TensorFlowjs实现体感格斗教程

当时实现方案很大略,也没有利用时下流行的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.01255), 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张照片,考虑到后面演习和评估时的运算量,我们减小了图片体积,每张图的分辨率都被压缩成10056。

建立模型

现在,我们开始建立图片分类模型。
处理图片利用的是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引用一个匿名函数。
在这个匿名函数中,我们把视频当前帧放入画布中,然后压缩成10056的图片后,再用于灰阶滤波器。

不才一步中,我们把压缩后的帧通报给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

— 完 —