基于ESP32的不智能手势检测系统

banner: https://www.pixiv.net/artworks/118998281

引言

伴随着少女乐队概念的兴起与不断发展,loT(lesbian of things)越来越多地出现在了人们的日常生活之中,从路边种植的黄瓜到每个人手腕上佩戴的企鹅创可贴,越来越多的睦头人被连接到互联网上,集成在了一起。通过一辈子的承诺来控制各种大小姐、重女等已经逐渐成为了公式化乐队生活起居的一部分,通过姛联网,这些成员能够更好地协调和运行,服务于我们的生活与工作。

╰(°▽°)╯╰(°▽°)╯╰(°▽°)╯╰(°▽°)╯╰(°▽°)╯

如果问上面的一段话和本文内容有什么关系,那答案是这就和MQTT和智能音箱的关系一样令人摸不着头脑。在翻页时钟写到一半时,我发现此物不易携带,被我暂置于家。而且孙神对我的玩具代码和用的合宙单片机嗤之以鼻,极大打击了我的信心,因此上面的那一篇只能先搁置下。近期我正好在嘉立创看到了赛博魔杖这个项目,便想着能不能用更低的成本复刻一下。

可行性验证

为了省钱,我的选择是用esp32-c3+mpu6500这两款相对廉价的芯片,核心成本可以控制在20以内。

元件 价格
ME6217C33M5G 0.5
TP4056 0.5
ESP32-C3 5
MPU6500 2
W25Q128 2
陶瓷天线 0.3
其他 <10

很快就用的是嘉立创送的4层沉金打好了板。由于我就买了几个芯片,其他的随便找一些替代了,所以看起来就有点奇怪。

esp32c3和mpu6500用的是5x5和3x3的qfn封装,孙神曾告诉我别用这种封装,直接买模块,因为连排针都焊不好的新手肯定焊不了这个。但我直接焊了一遍也没有发现啥问题。

不知道是不是因为我画这第一块板子的时候选了一个常用ldo的不常用封装,淘宝卖家给我发了一个错的封装过来。这让我在新的ldo没到之前,板子只能由外部供电,而不能从usb供电。可以用传统的usb转串口芯片,接四根线连板子,或者直接像我下面一样,用随便什么东西供电,而下载调试直接经过usb到c3内部的usb桥。

这样的好处就是不用按按键就能直接下载,而且波特率可以达到460800,不是一般的9600和115200能比的。

通过这个仓库,就可以看到实时姿态了。可以发现有的方向是反的,但对用于姿态检测的神经网络来说,应该无所谓。

卷积神经网络是一种深度学习模型,主要用于处理和分析图像数据。它的设计灵感源自于对生物视觉系统的理解,模拟了视觉皮层的结构和功能。CNN通过一系列的卷积层、池化层和全连接层构建而成,每一层都有特定的功能和作用。为了验证esp32的深度学习能力,设计一个网络。

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
class MyNetwork(nn.Module):
def __init__(self):
super().__init__()

# input:(bitch_size, 6, 50), output:(bitch_size, 18, 25)
self.conv1 = nn.Sequential(
nn.Conv1d(6, 18, 3, 1, 1),
nn.ReLU(inplace=True),
nn.MaxPool1d(kernel_size=2),
)

# input:(bitch_size, 18, 25), output:(bitch_size, 36, 12)
self.conv2 = nn.Sequential(
nn.Conv1d(18, 36, 3, 1, 1),
nn.ReLU(inplace=True),
nn.MaxPool1d(kernel_size=2),
)

self.fc1 = nn.Sequential(
nn.Linear(in_features=36 * 12, out_features=512),
nn.ReLU(inplace=True),
)

self.fc2 = nn.Sequential(
nn.Linear(in_features=512, out_features=128),
nn.ReLU(inplace=True),
)

self.fc3 = nn.Linear(in_features=128, out_features=3)

def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.shape[0], -1)
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x

可以看到,它具有两个卷积层和两个全连接层,最后一个分类输出层。我每隔10ms采集一次6500的各轴数据,连续采集50次,就是0.5s。将这些六轴数据作为输入,那么输入的形状就是6(features)*50(size)。简单起见,我这次就设定了三个手势,分别是顺时针旋转、逆时针旋转和不动,由分类输出层输出。每个手势我都采集了50秒,也即100组数据,以六四开分成训练集和测试集,在PyTorch上进行训练,结果如下显示。

只能说数据量太小,模型有点拟合地太好了。不到5秒时间,在loss双双降至0的同时,准确率也飙到了100。

接下来就是在esp32上部署模型了。据我所知,在esp32上官方已经支持的框架有两个,分别是esp-dl和TensorFlow Lite for Microcontrollers。我看了一下esp-dl,发现它支持的算子好像有点少(我超,OP!),所以就把重点放在后面一个。虽然如今的学界已经基本没人用TensorFlow了,但在业界PyTorch还是打不过TF。我选择使用资料相对较多的TensorFlow Lite for Microcontrollers来作为模型推理的运行时。要将PyTorch模型转换为TensorFlow Lite模型,大抵需要以下一些步骤:

PyTorch模型->ONNX模型->TensorFlow模型->TensorFlow Lite模型

终于转换完成之后,却发现最后未经量化的float32模型大小是1141KB。这个对于嵌入式设备来说还是有点太大了。看来神经元还是有点太多了,优化到9KB甚至10KB以内才能让人接受。

于是我把模型改成了这样:

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
class MyNetwork(nn.Module):

def __init__(self):
super().__init__()

# input:(bitch_size, 6, 50), output:(bitch_size, 8, 25)
self.conv1 = nn.Sequential(
nn.Conv1d(6, 8, 3, 1, 1),
nn.ReLU(inplace=True),
nn.MaxPool1d(kernel_size=2),
)

self.fc1 = nn.Sequential(
nn.Linear(in_features=8 * 25, out_features=20),
nn.ReLU(inplace=True),
)

self.fc2 = nn.Linear(in_features=20, out_features=3)

def forward(self, x):
x = self.conv1(x)
x = x.view(x.shape[0], -1)
x = self.fc1(x)
x = self.fc2(x)
return x

终于,最后的模型从1M变到了默认23K,勉强能用,而准确率也掉到了95%左右。虽然相对来说,TensorFlow Lite是属于资料比较多的嵌入式深度学习框架,但我发现不管是国内外,除了官网的那点介绍和例程,其他的资料都不多。模型部署到esp32上的过程和坑将在下文详细说明,这里直接给出结果:

上面的视频我又改了一下模型,视频中模型大小为70KB,显然优化空间很大。esp32c3单次推理用时约52ms,由于输入输出张量较小,推理时内存占用基本和模型大小差不多。
至此,该项目的核心:在esp32上运行一个神经网络可行性验证已经完成。

设计思路

成本优先

价格对于我来说还是比较敏感的,像这种项目我认为控制在30以内比较好。

低耦合、组件化、可扩展

这块板子尽量按照多种功能来设计。首先可以是一块纯粹的esp32开发板,加上音频模块可以学习开发音频,加上陀螺仪可以学习姿态检测,加上红外收发功能可以变成遥控器。如果不加,多出来的IO也全部引出可做它用。对于焊接条件不是很好的同学,也可以买来模块连在板子上。