发布于 2026-01-05 2 阅读
0

使用 Arduino 和 Tensorflow.js 实现体感操作的街头霸王游戏演示所需材料步骤 1:数据收集步骤 2:数据处理训练模型使用模型进行预测其他

使用 Arduino 和 Tensorflow.js 通过身体动作玩街头霸王

演示

所需材料

第一步:收集数据

步骤二:数据处理

训练模型

利用该模型进行预测

额外内容

过去一年,我一直在学习机器学习。我做了一些浏览器实验,但最近,我花了一些时间将机器学习与我的另一个爱好——硬件——结合起来!

以下教程介绍了我是如何使用 Arduino 和Tensorflow.js构建手势识别系统的原型。

这只是第一个版本,目前仍处于实验阶段。

接下来将介绍我构建这个项目的主要步骤,并提供一些代码示例。
如果您想查看完整的代码,可以访问 GitHub 仓库,但请注意,在接下来的几个月里,我会对其中的大部分代码进行修改。

我认为最重要的是理解需要采取的步骤,而不是理解所有需要的代码。


演示

这个原型旨在训练一个机器学习模型来识别“拳击”或“波动拳”等身体动作,以便与街头霸王网页游戏进行交互。

最终结果如下:

用身体动作玩街头霸王

这个项目的灵感来源于Minko Gechev使用网络摄像头进行的类似项目。


所需材料

要搭建这个原型,我们需要一些硬件。我制作原型时用到的硬件包括:

我用面包板把所有元件组装在一起,但如果你决定焊接,你可能需要一个原型板。

技术栈方面,我使用了:

  • 原生 JavaScript
  • Tensorflow.js
  • Node.js
  • 强尼五
  • Web套接字

第一步:收集数据

如果我们从零开始,我们只有这个想法:用肢体动作来玩街头霸王。现在,我们需要考虑如何实现这个想法……

为了构建分类器,我们需要数据。这些数据将来自我们即将握在手中的硬件设备。

要获取数据,我们需要先将各个组件组装在一起。

它应该看起来像这样:

Arduino 草图

我使用的微控制器是 Arduino MKR1000。我选择这款是因为我家里正好有,而且它内置了 Wi-Fi,这意味着我不需要一直连着电脑就能记录手势。你也可以试试 Arduino Uno,但那样你就得一直连着电脑,这对于这个原型来说不太理想,不过对于入门来说仍然很有用。

第二主要部分是加速度计/陀螺仪。我使用的是 MPU6050,它可以获取 x、y 和 z 轴的加速度数据,以及 x、y 和 z 轴的旋转数据,总共可以提供 6 个数据点。

最后,我还使用了一个按钮,因为我只想在做出特定手势时才记录数据。这样,我就可以只在按下按钮并做出例如“出拳”动作时才记录数据。

现在我们已经组装好了硬件,接下来需要编写代码来获取这些数据。

为此,我使用了 Johnny-Five 框架,通过 JavaScript 在我的计算机和 Arduino 之间进行通信。

代码大致如下:

const EtherPortClient = require("etherport-client").EtherPortClient;
const five = require('johnny-five');
const fs = require('fs');

const board = new five.Board({
    port: new EtherPortClient({
      host: "192.168.1.113", //Your Arduino IP goes here
      port: 3030
    }),
    timeout: 1e5,
    repl: false
});

board.on("ready", function() {
    const button = new five.Button("A0");

    let stream = fs.createWriteStream(`data/sample_punch_0.txt`, {flags: 'a'});

    const imu = new five.IMU({
        pins: [11,12], // connect SDA to 11 and SCL to 12
        controller: "MPU6050"
    });

    imu.on("data", function() {
        let data = `${this.accelerometer.x} ${this.accelerometer.y} ${this.accelerometer.z} ${this.gyro.x} ${this.gyro.y} ${this.gyro.z}`;

        button.on("hold", () => stream.write(`${data} \r\n`));
    });

    button.on("release", () => stream.end());
});

在上面的代码示例中,我们首先引入所需的 Node.js 模块,然后设置开发板,包括 Arduino 的 IP 地址和通信端口。接下来,当开发板准备就绪后,我们连接按钮和 MPU6050 传感器,并创建一个数据流,以便将所有数据写入文件。当从传感器获取数据时,我们将其存储在一个变量中,并在按住按钮的同时,将数据写入之前声明的文件。
最后,当松开按钮时,我们关闭数据流,这意味着不再向该文件写入数据。

这段代码示例演示了如何将单个手势样本的数据写入文件。然而,对于每个手势,我们需要记录多个样本,因此您需要修改此文件以记录第 2 个拳击样本、第 3 个拳击样本、第 4 个拳击样本等等。

完成手势数据记录这一步骤后,我们需要进行第二步才能使用它,这一步是数据处理。


步骤二:数据处理

目前,我们只有一个文件夹,里面全是传感器数据文件,看起来应该像这样:

0.40205128205128204 0.019145299145299145 -4.384273504273504 0.06110144116383567 -0.27059209658270084 0.3578798696738946
-0.13401709401709402 -0.5743589743589743 -3.561025641025641 0.008728777309119381 -0.3578798696738946 0.6546582981839536
-1.3210256410256411 -0.47863247863247865 -3.1398290598290597 -0.22694821003710391 -0.026186331927358142 0.8117762897481025
-1.7230769230769232 -0.1723076923076923 -2.9675213675213676 -0.6895734074204312 0.183304323491507 0.20949065541886513
-1.3593162393162392 -0.4211965811965812 -3.024957264957265 -0.9252503947666544 0.21821943272798452 -0.28804965120093956
-1.4167521367521367 -0.5360683760683761 -2.7377777777777776 -0.9601655040031319 0.3229647604374171 -0.1396604369459101
-2.201709401709402 -0.22974358974358974 -2.3165811965811964 -1.0125381678578482 0.45389642007420783 0.1309316596367907
-3.1015384615384614 0.09572649572649572 -1.7996581196581196 -1.1958424913493553 0.6721158528021923 0.06110144116383567 
-3.2164102564102564 0.6892307692307692 -1.435897435897436 -1.483892142550295 1.0125381678578482 -0.08728777309119382 
-3.407863247863248 1.6464957264957265 -1.1678632478632478 -1.7195691298965181 1.187113714040236 -0.24440576465534267 
-3.963076923076923 1.991111111111111 -0.7466666666666667 -1.8766871214606669 1.1347410501855195 -0.21821943272798452 
-5.322393162393162 4.1928205128205125 1.1678632478632478 -2.2869396549892778 1.9290597853153832 0.39279497891037213 
-5.264957264957265 6.337094017094017 1.9336752136752138 -2.609904415426695 2.3043972096075165 -0.07855899578207443 
-4.843760683760684 7.275213675213675 2.508034188034188 -2.8455814027729183 2.356769873462233 -0.8554201762936994 
-4.5948717948717945 7.102905982905983 3.063247863247863 -2.976513062409709 2.496430310408143 -1.1521986048037582
-2.1442735042735044 9.649230769230769 3.6184615384615384 -3.4478670371021556 3.1685461632103356 -0.6546582981839536

为了能够使用它,我们需要从这些文件中读取数据,并将其转换以便 Tensorflow.js 可以使用。

1. 从文件中读取数据

我不会详细讲解实现此功能的代码,因为我认为很多其他博客文章都已经介绍过,而且我相信如果你需要的话,你也能自己弄明白。

主要目标是遍历数据文件夹中的每个数据文件,逐行读取并将数据从上述格式转换为对象数组。

我们希望数据看起来像这样:

{ features:
   [ -0.11487179487179487, 9.63008547008547, -4.345982905982906, -0.22694821003710391, 0.04364388654559691, 0.5586417477836404, -0.07658119658119658, 9.074871794871795, -4.7671794871794875,0.11347410501855196, 0.08728777309119382, 0.8990640628392963,
-0.7658119658119658, 9.744957264957264, -4.288547008547009, 0.052372663854716284, -0.1309316596367907, 0.7768611805116249, -1.3784615384615384, 9.610940170940172, -3.790769230769231, -0.017457554618238762, -0.2618633192735814, 0.34915109236477526,
-2.4697435897435898, 9.725811965811966, -3.6567521367521367, -0.10474532770943257, -0.17457554618238763, -0.034915109236477525, -3.58017094017094, 9.898119658119658,
-3.9056410256410254, -0.07855899578207443, -0.06983021847295505, -0.296778428510059,     -4.7097435897435895, 9.993846153846153, -3.9247863247863246, -0.07855899578207443,
-0.04364388654559691, -0.5411841931654017, -6.04991452991453, 10.08957264957265,
-3.9439316239316238, -0.06110144116383567, 0.034915109236477525,-0.6459295208748342, 
... 260 more items ],
  label: 1 }

我们现在要做的是将文件中的几行数据转换sample_punch_0.txt成我们可以直接使用的数据。
特征数组代表单个手势样本的数据,标签则代表手势的名称。

我们不想使用字符串,所以如果我们想训练 3 种不同的手势,我们可以使用一个手势数组 ['波动拳', '拳击', '上勾拳']。在这种情况下,标签 1 将映射到 '拳击'。

不过,我们需要对所有数据文件都进行这样的操作,所以最终我们会得到一个庞大的手势对象数组,如下所示:

[
{ features:
   [ -0.11487179487179487, 9.63008547008547, -4.345982905982906, -0.22694821003710391, 0.04364388654559691, 0.5586417477836404, -0.07658119658119658, 9.074871794871795, -4.7671794871794875,0.11347410501855196, 0.08728777309119382, 0.8990640628392963,
... 530 more items ],
  label: 1 },
{ features:
   [ -0.11487179487179487, 9.63008547008547, -4.345982905982906, -0.22694821003710391, 0.04364388654559691, 0.5586417477836404, -0.07658119658119658, 9.074871794871795, -4.7671794871794875,0.11347410501855196, 0.08728777309119382, 0.8990640628392963,
... 530 more items ],
  label: 0 },
{ features:
   [ -0.11487179487179487, 9.63008547008547, -4.345982905982906, -0.22694821003710391, 0.04364388654559691, 0.5586417477836404, -0.07658119658119658, 9.074871794871795, -4.7671794871794875,0.11347410501855196, 0.08728777309119382, 0.8990640628392963,
... 530 more items ],
  label: 2 },
{ features:
   [ -0.11487179487179487, 9.63008547008547, -4.345982905982906, -0.22694821003710391, 0.04364388654559691, 0.5586417477836404, -0.07658119658119658, 9.074871794871795, -4.7671794871794875,0.11347410501855196, 0.08728777309119382, 0.8990640628392963,
... 530 more items ],
  label: 2 },
...
]

我们现在已经将所有文件转换为标签和特征对象。

然而,这还不能直接用于TensorFlow。我们需要继续将数据转换成该框架可以使用的格式。

2.数据格式化

在这个阶段,我们将开始把对象转换成两个数组。一个用于标签,一个用于特征。

我们的目标是这样的:

// labels
[ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
  [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
  [ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 ] ]

// features
[ 
  [ 
    [ 5.686153846153847, ... 400 more items ], [ 9.285470085470086,... 200 more items ], ... 
  ],
  [ 
    [ 5.686153846153847, ... 400 more items ], [ 9.285470085470086,... 200 more items ], ... 
  ],
  [ 
    [ 5.686153846153847, ... 400 more items ], [ 9.285470085470086,... 200 more items ], ...
  ],
]

通过上述格式,我们将标签和特征分开,但它们之间仍然相互关联。也就是说,标签数组的第一层代表所有标签为 0 的手势(例如“波动拳”),而特征数组的第一层则代表波动拳手势的所有数据。

再次声明,我不会展示具体的代码实现方式,因为到目前为止,这与Tensorflow.js本身并无直接关系。我们所做的只是使用JavaScript数组方法,将文件中的行数据转换为对象,最终转换为多维数组。

现在,我们离 Tensorflow 可以处理的东西已经非常接近了,只是该框架使用一种称为张量的特殊数据结构。

3. 转换为张量

接下来我们将开始使用Tensorflow.js的具体代码。我们将使用内置方法将数组转换为张量。

为此,这里提供一个代码示例:

function convertToTensors(featuresData, labelData) {
  // we start by shuffling our data so our model doesn't get used to the way we feed it data.
  const [shuffledFeatures, shuffledLabels] = shuffleData(featuresData, labelData);

  // numSamplesPerGesture is the number of times we trained a single gesture (e.g. we trained the "punch" gesture 20 times)
  // totalNumDataPerFile is the number of data points we take into consideration per gesture. If we only consider the first 50 lines of a data file, 50 * 6 points of data = 300; 
  const featuresTensor = tf.tensor2d(shuffledFeatures, [numSamplesPerGesture, totalNumDataPerFile]); 

  // 1D tensor for labels & convert them from the set [0, 1, 2] into one-hot encoding (.e.g., 0 --> [1, 0, 0]). 
  // e.g: punch at index 0 ⇒ [1,0,0], hadoken at index 1 ⇒ [0,1,0], 
  const labelsTensor = tf.oneHot(tf.tensor1d(shuffledLabels).toInt(), numClasses);
}

现在我们有了标签张量和特征张量。我们几乎可以开始训练模型了!但首先,最后一步是将数据拆分为训练集和测试集。

4. 分裂

考虑到我们上面的代码示例,我们需要将标签张量和特征张量拆分为训练集和测试集。

我们这样做的原因是,我们希望使用大约 80% 的数据集来训练模型,而将剩余的 20% 的数据集用于验证模型的预测结果。

const numTestExamples = Math.round(numSamplesPerGesture * 0.2); // 20% 
const numTrainExamples = numSamplesPerGesture - numTestExamples; // 80%

// Split between training set and test set.
const trainingFeatures = featuresTensor.slice([0, 0], [numTrainExamples, totalNumDataPerFile]);

const testingFeatures = featuresTensor.slice([numTrainExamples, 0], [numTestExamples, totalNumDataPerFile]);

const trainingLabels = labelsTensor.slice([0, 0], [numTrainExamples, numClasses]);

const testingLabels = labelsTensor.slice([numTrainExamples, 0], [numTestExamples, numClasses]);

return [trainingFeatures, trainingLabels, testingFeatures, testingLabels];

现在我们有了标签和特征的训练张量和测试张量,就可以创建模型了。


训练模型

构建模型这一步骤比之前的步骤更具实验性。你可以用很多不同的方式构建模型,并且可以尝试调整参数,例如神经网络的层数、训练轮数(步数)等等。

创建理想模型并没有固定的方法。随着参数的调整,模型的准确率和预测结果应该会发生变化,一旦达到满意的准确度,就可以停止调整。

我目前的模型是这样创建的:

const createModel = async (trainingFeatures, trainingLabels, testFeatures, testLabels) => {
   const params = { learningRate: 0.1, epochs: 40 };
   const model = tf.sequential();
   model.add(tf.layers.dense({ units: 10, activation: 'sigmoid', inputShape: [trainingFeatures.shape[1]] }));
   model.add(tf.layers.dense({ units: 3, activation: 'softmax' }));

   const optimizer = tf.train.adam(params.learningRate);

   model.compile({
       optimizer: optimizer,
       loss: 'categoricalCrossentropy',
       metrics: ['accuracy'],
   });

   await model.fit(trainingFeatures, trainingLabels, {
       epochs: params.epochs,
       validationData: [testFeatures, testLabels],
   });

   await model.save(file://model’);
}

此代码示例的最后一行会将模型保存为应用程序中的一个文件。这样,您就可以在最后一步中使用它来预测新的数据样本!


利用该模型进行预测

现在训练过程已经完成,我们的模型已经可以用来对“波动拳”、“拳击”和“上勾拳”之间的新样本进行分类。

const tf = require('@tensorflow/tfjs-node');
let liveData = [];
let model;
const gestureClasses = ['hadoken', 'punch', 'uppercut'];

const init = async () => {
   model = await tf.loadLayersModel('file://model/model.json');
}

// similar step to the recording process
imu.on("data", function() {
   button.on("hold", () => {
       let data = {xAcc: this.accelerometer.x, yAcc: this.accelerometer.y, zAcc: this.accelerometer.z,
                  xGyro: this.gyro.x, yGyro: this.gyro.y, zGyro: this.gyro.z};

       if (liveData.length < numValuesExpected){
           liveData.push(data.xAcc, data.yAcc, data.zAcc, data.xGyro, data.yGyro, data.zGyro)
       }
   });

   button.on("release", function(){
     predict(model, liveData);
     liveData = [];
   });
});

const predict = (model, newSampleData) => {
   tf.tidy(() => {
       // the live data we get is just an array of numbers. We also need to transform it to a tensor so the model can use it.
       const input = tf.tensor2d([newSampleData], [1, 300]);
       const prediction = model.predict(input);
       // the prediction will come back as an index, the same as our labels in our data set.
       const gesturePredicted = gestureClasses[prediction.argMax(-1).dataSync()[0]];

    console.log(gesturePredicted) // either punch, hadoken or uppercut;
    });
}

通过上面的代码示例,我们可以在按住按钮并执行我们训练过的多种手势之一时获取实时数据。松开按钮后,我们会使用predict模型从未见过的新样本运行函数。我们会得到一个索引,该索引可用于在gestureClasses数组中查找预测的手势。

我们完成了!🎉


额外内容

正如我在本教程开头所说,最重要的是理解如果你想构建类似的东西需要遵循哪些步骤。如果你不能完全理解代码,完全没关系!

值得一提的是,首先你需要一种获取数据的方法,但这并不一定非要用到 Arduino。我用 Daydream 控制器和……一部手机搭建了一个类似的项目!

大多数现代手机都内置了加速度计和陀螺仪,可用于收集此类实验的数据。代码需要稍作修改,因为您需要使用通用传感器 API而不是 Johnny-Five,代码大致如下:

let gyroscope = new Gyroscope({frequency: 60});

gyroscope.addEventListener('reading', e => {
   // gyroscope.x;
   // gyroscope.y;
   // gyroscope.z;
});
gyroscope.start();

let accelerometer = new Accelerometer({frequency: 60});

accelerometer.addEventListener('reading', e => {
   // accelerometer.x;
   // accelerometer.y;
   // accelerometer.z;
});
accelerometer.start();

如果您想尝试一下,这里有一个演示,您可以在此仓库中找到代码


之后我可能会再写一篇博文,因为我会逐步改进代码,并最终开展其他实验 :)

感谢阅读!💚

文章来源:https://dev.to/devdevcharlie/play-street-fighter-with-body-movements-using-arduino-and-tensorflow-js-4kbi