分任务5:AI功能应用——结合运动传感器,完成手势识别功能,至少要识别三种手势(如水平左右、前后、垂直上下、水平画圈、垂直画圈,或者更复杂手势
这个任务可难,可简单我也有两个思路。
①就是简单的判断加速度xyz轴是否超过规定的阈值,从而实现上下左右前后,6个方向的识别。买来的LIS3DH是个三轴加速度计,有STEMMAQT接口,买好对应的线可以直接和板子上的接口接在一起,其实就是IIC通信接口,很方便。但是注意的是,一开始我在淘宝搜的时候就搜索的jst SH 4 pin,买回来后插上去LIS3DH的led灯不亮,原因是这个线是同向的,而其实是要求反向的。这么说大家可能没理解,大概意思是二esp32的gnd,vcc,sda,scl连接了传感器的scl,sda,vcc,gnd。第二次我重新买线,搜索的是STEMMAQT并观察了接口方向才买的,倒霉的是一根线几毛钱,运费却要4块😔。
第二次买来的线是对的,接上去就亮了。首先我们尝试读取数据,有官方的库提供参考。
效果:注意区别方向,这里我是x轴正方向朝右
可以看到,随着传感器只朝者一个方向来回运动的时候,某一轴的加速度会变化。
数据读取代码:
import time
import board
import busio
import adafruit_lis3dh
i2c = board.I2C()
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c)
# Set range of accelerometer (can be RANGE_2_G, RANGE_4_G, RANGE_8_G or RANGE_16_G).
lis3dh.range = adafruit_lis3dh.RANGE_2_G
while True:
# Read accelerometer values (in m / s ^ 2). Returns a 3-tuple of x, y,
# z axis values. Divide them by 9.806 to convert to Gs.
x, y, z = [
value / adafruit_lis3dh.STANDARD_GRAVITY for value in lis3dh.acceleration
]
print("x = %0.3f G, y = %0.3f G, z = %0.3f G" % (x, y, z))
# Small delay to keep things responsive but give time for interrupt processing.
time.sleep(0.1)
接下来,我们就给每个方向设置阈值,把识别的方向通过屏幕显示出来,本来我也准备了rgb小灯的亮灭来展示识别的方向的,奈何视像头太差了拍不出效果。
简单动作识别效果:
可以看出,确实可以识别动作,但是影响太大。
代码:
import time
import board
import busio
import adafruit_lis3dh
import displayio
from adafruit_display_text import label
from adafruit_st7789 import ST7789
from adafruit_bitmap_font import bitmap_font
#-----------------------显示设置-------------------------------------------------
# First set some parameters used for shapes and text
BORDER = 20
FONTSCALE = 3
BACKGROUND_COLOR = 0x000000 # Bright Green
TEXT_COLOR = 0xffffff
font_file = "wenquanyi_13px.pcf"
font = bitmap_font.load_font(font_file)
# Release any resources currently in use for the displays
displayio.release_displays()
spi = board.SPI()
tft_cs = board.TFT_CS
tft_dc = board.TFT_DC
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs)
display = ST7789(
display_bus, rotation=270, width=240, height=135, rowstart=40, colstart=53
)
# Make the display context
splash = displayio.Group()
display.show(splash)
color_bitmap = displayio.Bitmap(display.width, display.height, 1)
color_palette = displayio.Palette(1)
color_palette[0] = BACKGROUND_COLOR
bg_sprite = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, x=0, y=0)
splash.append(bg_sprite)
# Draw a label
text = "动作识别"
text_area = label.Label(font, text=text, color=TEXT_COLOR)
text_width = text_area.bounding_box[2] * FONTSCALE
text_group = displayio.Group(
scale=FONTSCALE,
x=display.width // 2 - text_width // 2,
y=display.height // 2,
)
text_group.append(text_area) # Subgroup for text scaling
splash.append(text_group)
#------------------------------分割线-----------------------------------------------------
#传感器通信初始化
i2c = board.I2C()
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c)
lis3dh.range = adafruit_lis3dh.RANGE_2_G
time.sleep(2)
text_area.text = '开始'
text_group.scale = 8
while True:
acc_x, acc_y, acc_z = [value / adafruit_lis3dh.STANDARD_GRAVITY for value in lis3dh.acceleration]
text_group.scale = 8
if acc_x >= 1:
# print('right')
text_area.text = '右'
time.sleep(1)
elif acc_x <= -1:
# print('left')
text_area.text = '左'
time.sleep(1)
elif acc_y >= 1:
# print('qian')
text_area.text = '前'
time.sleep(1)
elif acc_y <= -1:
# print('hou')
text_area.text = '后'
time.sleep(1)
elif acc_z >= 1.8:
# print('up')
text_area.text = '上'
time.sleep(1)
elif acc_z <= 0.2:
# print('down')
text_area.text = '下'
time.sleep(1)
# print(acc_x, acc_y, acc_z)
②接下来使用DTW算法,这个算是AI功能应用了,我觉得所谓AI其实就是算法+数据。DTW算法简单来说,就是比较序列之间的相似性,记录测试动作为一段时间序列,其内容是三轴加速度数据,通过比较测试动作的序列与已知动作的序列,找到最相似的,从而实现动作识别。
我主要参考的资料:bugyu_ld的个人空间_哔哩哔哩_bilibili,树莓派-MPU6050+DTW算法实现简单动作识别_哔哩哔哩_bilibili。原作者使用的是树莓派+MPU6050,这里我使用的是ESP32S3+LIS3DH,原作者使用python编程,这里我将其代码移植了过来,可以使用micropython/circuitpython跑,由于后两者都是python3语法,改动起来很方便,如库的区别,比如要使用的是ulab里的numpy库等等内容。
特别注意,由于需要向esp32中写数据。需要禁用u盘模式,否者无法写入文件。操作也很简单,在boot.py中:
加入storage.disable_usb_drive()语句就可以了。
接下来运行主函数,第一次运行的话,我们需要先录制动作,输入动作名称,记录动作,数据保存在npy_data文件夹里(需要自己先创建个空文件夹),文件结尾是.npy其实就是npy数组,这个形式可以存储也可以方便numpy计算。
在原作者的程序里,是可以将一个动作录制多次的,最后会计算一个动作多个序列的平均值作为距离评估。但是由于单片机和micropython/circuitpython性能有限,这里我就将一个动作只记录一次了。而且如果记录的数据过多,计算起来是真的很慢,就像上面我有10组数据,计算起来10几20秒都有了吧。
所以这里我是我将分为两组动作演示,一个是简单动作组:上下左右前后;一个是复杂动作组:圆形,三角形,方形,五角星,垂直的圆。
简单动作DTW算法识别:
由于down一直没识别出来所以就重新录了一次。
复杂动作DTW算法识别:
可以看出,识别效果不是很佳,但是能跑就行!😂🤣🤡
代码:
from ulab import numpy as np
import os
import adafruit_lis3dh
import time
import board
def MIN(a,b,c):
return np.min([a,b,c])
def dis_abs(x, y):
return abs(x-y)[0]
def NORM(x):
return np.sqrt(np.dot(x,x))
def dis_ACC(x,y):
scale = 0.5
diff = x-y
dis = np.dot(x,y)/( NORM(x) * NORM(y) + 1e-8)
dis = (1-scale*dis) * NORM(diff)
return dis
def estimate_dtw(A,B,dis_func=dis_ACC):
N_A = len(A)
N_B = len(B)
D = np.zeros([N_A,N_B])
D[0,0] = dis_func(A[0],B[0])
# 左边一列
for i in range(1,N_A):
D[i,0] = D[i-1,0]+dis_func(A[i],B[0])
# 下边一行
for j in range(1,N_B):
D[0,j] = D[0,j-1]+dis_func(A[0],B[j])
# 中间部分
for i in range(1,N_A):
for j in range(1,N_B):
D[i,j] = dis_func(A[i],B[j])+min(D[i-1,j],D[i,j-1],D[i-1,j-1])
# 路径回溯
i = N_A-1
j = N_B-1
count =0
d = np.zeros(max(N_A,N_B)*3)
path = []
while True:
if i>0 and j>0:
path.append((i,j))
m = min(D[i-1, j],D[i, j-1],D[i-1,j-1])
if m == D[i-1,j-1]:
d[count] = D[i,j] - D[i-1,j-1]
i = i-1
j = j-1
count = count+1
elif m == D[i,j-1]:
d[count] = D[i,j] - D[i,j-1]
j = j-1
count = count+1
elif m == D[i-1, j]:
d[count] = D[i,j] - D[i-1,j]
i = i-1
count = count+1
elif i == 0 and j == 0:
path.append((i,j))
d[count] = D[i,j]
count = count+1
break
elif i == 0:
path.append((i,j))
d[count] = D[i,j] - D[i,j-1]
j = j-1
count = count+1
elif j == 0:
path.append((i,j))
d[count] = D[i,j] - D[i-1,j]
i = i-1
count = count+1
mean = np.sum(d) / count
return mean, path[::-1],D
def mov_record(MPU):
data_record = []
print('请在1.5s内完成动作')
time.sleep(0.1)
for i in range(50):
time.sleep(0.03)
accs= [value / adafruit_lis3dh.STANDARD_GRAVITY for value in lis3dh.acceleration]
data_record.append(accs)
print('动作结束')
return data_record
def get_mov_list():
try:
os.chdir('npy_data')
except:
pass
list_files = []
list_labs = []
for i in os.listdir():
if i.endswith('.npy'):
list_files.append(i)
for file in list_files:
lab = file.split('.')[0]
list_labs.append(lab)
return list_files,list_labs
if __name__ == "__main__":
try:
os.chdir('npy_data')
except:
pass
i2c = board.I2C()
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c)
lis3dh.range = adafruit_lis3dh.RANGE_2_G
try:
while True:
print('1 录制 2 测试 3 退出')
opt = input()
if opt == '1':
str_mov = input('请输入动作名称')
print('3 秒后开始录制动作 %s'%(str_mov))
time.sleep(3)
# 录制动作
mov_data = mov_record(lis3dh)
# 进行保存
file_name = str(str_mov)+".npy"
np.save(file_name,np.array(mov_data))
continue
elif opt == '2':
print('3 秒后开始录制测试动作')
time.sleep(3)
test_mov = mov_record(lis3dh)
print('计算结果中,请耐心等待')
scores = []
files,labs = get_mov_list()
if len(files)<1:
print('没有找到存储动作,请先进行动作录制')
continue
# 计算捕捉的动作与存储动作的距离
for file in files:
# 加载动作数据
data_train = np.load(file)
# 计算测试动作与存储动作之间的距离
score,_,_ = estimate_dtw(np.array(test_mov),data_train)
scores.append(score)
# 找到最小值的距离
for lab in labs:
print('{:^9}'.format(lab),end=' ')
print('\n')
for lab in labs:
print('{:^9}'.format('|'),end=' ')
print('\n')
print(scores)
index = scores.index(min(scores))
print('测试动作为 '+labs[index])
continue
elif opt == '3':
break
except KeyboardInterrupt:
print('\n Ctrl + C QUIT')
第二部分:MPU6050
MPU6050可以记录三轴加速度,三轴加速度。所以上面的工作它也可以做,这里就不多赘述了。官方资料里搜索MPU6050就可以实现读取其6轴信息还有温度,再结合我上面的步骤也可以实现功能。注意单位区别,好像两个传感器读取的数据范围不太一样,我记得LIS3DH代码会除去重力作用吧(除以重力),我没太注意,但发现有区别。
下面是官方的例子读取MPU6050代码:
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
import time
import board
import adafruit_mpu6050
i2c = board.I2C() # uses board.SCL and board.SDA
# i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller
mpu = adafruit_mpu6050.MPU6050(i2c)
while True:
print("Acceleration: X:%.2f, Y: %.2f, Z: %.2f m/s^2" % (mpu.acceleration))
print("Gyro X:%.2f, Y: %.2f, Z: %.2f rad/s" % (mpu.gyro))
print("Temperature: %.2f C" % mpu.temperature)
print("")
time.sleep(1)
mpu6050 dtw算法代码:
from ulab import numpy as np
import os
import uuid
import adafruit_mpu6050
import time
import board
def MIN(a,b,c):
return np.min([a,b,c])
def dis_abs(x, y):
return abs(x-y)[0]
def NORM(x):
return np.sqrt(np.dot(x,x))
def dis_ACC(x,y):
scale = 0.5
diff = x-y
dis = np.dot(x,y)/( NORM(x) * NORM(y) + 1e-8)
dis = (1-scale*dis) * NORM(diff)
return dis
def estimate_dtw(A,B,dis_func=dis_ACC):
N_A = len(A)
N_B = len(B)
D = np.zeros([N_A,N_B])
D[0,0] = dis_func(A[0],B[0])
# 左边一列
for i in range(1,N_A):
D[i,0] = D[i-1,0]+dis_func(A[i],B[0])
# 下边一行
for j in range(1,N_B):
D[0,j] = D[0,j-1]+dis_func(A[0],B[j])
# 中间部分
for i in range(1,N_A):
for j in range(1,N_B):
D[i,j] = dis_func(A[i],B[j])+min(D[i-1,j],D[i,j-1],D[i-1,j-1])
# 路径回溯
i = N_A-1
j = N_B-1
count =0
d = np.zeros(max(N_A,N_B)*3)
path = []
while True:
if i>0 and j>0:
path.append((i,j))
m = min(D[i-1, j],D[i, j-1],D[i-1,j-1])
if m == D[i-1,j-1]:
d[count] = D[i,j] - D[i-1,j-1]
i = i-1
j = j-1
count = count+1
elif m == D[i,j-1]:
d[count] = D[i,j] - D[i,j-1]
j = j-1
count = count+1
elif m == D[i-1, j]:
d[count] = D[i,j] - D[i-1,j]
i = i-1
count = count+1
elif i == 0 and j == 0:
path.append((i,j))
d[count] = D[i,j]
count = count+1
break
elif i == 0:
path.append((i,j))
d[count] = D[i,j] - D[i,j-1]
j = j-1
count = count+1
elif j == 0:
path.append((i,j))
d[count] = D[i,j] - D[i-1,j]
i = i-1
count = count+1
mean = np.sum(d) / count
return mean, path[::-1],D
def mov_record(MPU):
data_record = []
print('请在1.5s内完成动作')
time.sleep(0.1)
for i in range(50):
time.sleep(0.03)
accs=MPU.acceleration
data_record.append(accs)
print('动作结束')
return data_record
def get_mov_list():
try:
os.chdir('npy_data')
except:
pass
list_files = []
list_labs = []
for i in os.listdir():
if i.endswith('.npy'):
list_files.append(i)
for file in list_files:
lab = file.split('.')[0]
list_labs.append(lab)
return list_files,list_labs
if __name__ == "__main__":
try:
os.chdir('npy_data')
except:
pass
i2c = board.I2C()
m_MPU = adafruit_mpu6050.MPU6050(i2c)
try:
while True:
print('1 录制 2 测试 3 退出')
opt = input()
if opt == '1':
str_mov = input('请输入动作名称')
print('3 秒后开始录制动作 %s'%(str_mov))
time.sleep(3)
# 录制动作
mov_data = mov_record(m_MPU)
# 进行保存
file_name = str(str_mov)+".npy"
np.save(file_name,np.array(mov_data))
continue
elif opt == '2':
print('3 秒后开始录制测试动作')
time.sleep(3)
test_mov = mov_record(m_MPU)
scores = []
files,labs = get_mov_list()
if len(files)<1:
print('没有找到存储动作,请先进行动作录制')
continue
# 计算捕捉的动作与存储动作的距离
for file in files:
# 加载动作数据
data_train = np.load(file)
# 计算测试动作与存储动作之间的距离
score,_,_ = estimate_dtw(np.array(test_mov),data_train)
scores.append(score)
# 找到最小值的距离
print(scores)
index = scores.index(min(scores))
print('测试动作为 '+labs[index])
continue
elif opt == '3':
break
except KeyboardInterrupt:
print('\n Ctrl + C QUIT')
mpu6050可以读取6轴信息,可以拿来做姿态解算,就是算出俯仰角、翻滚角和航向角。
参考链接:FallPrevent: Raspberry-pico micro-python读取mpu6050 老人防跌倒 (gitee.com),GitHub - narutogo/micropython-mpu6050-: micropython mpu6050 用四元数或者一节互补滤波解算角度。
解算部分我当然是不会的啦,主要就是大概看了下原理,云里雾里的,然后使用了大佬的库,自己随便写了点代码,然后通过串口传输给电脑,电脑上位机展示效果。
上位机参考链接:esp32系列(11):ESP32 IDF平台 mpu6050 DMP 驱动移植及测试上位机开发_esp-idf写mpu6050驱动_lu-ming.xyz的博客-CSDN博客
效果展示:效果一般,我的评价是能用就行。俯仰角,翻滚角倒是还能用,航向角就效果很差了,看资料说如果配合磁力计得到9轴陀螺仪数据再加上一个好的算法,姿态解算结果是很好的。
代码:主函数
from zitai_slove import Update_IMU
from q4 import IMUupdate
from One_filter import one_filter
import board
import busio
uart = busio.UART(board.TX, board.RX, baudrate=115200, receiver_buffer_size=2048)
import time
import board
import adafruit_mpu6050
i2c = board.I2C() # uses board.SCL and board.SDA
# i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller
mpu = adafruit_mpu6050.MPU6050(i2c)
while True:
acc_x, acc_y, acc_z = [value for value in mpu.acceleration]
gyro_x, gyro_y, gyro_z = [value for value in mpu.gyro]
# print('加速度', acc_x, acc_y, acc_z, '\n', '角速度', gyro_x, gyro_y, gyro_z)
# print("Acceleration: X:%.2f, Y: %.2f, Z: %.2f m/s^2" % (mpu.acceleration))
# print("Gyro X:%.2f, Y: %.2f, Z: %.2f rad/s" % (mpu.gyro))
# print("Temperature: %.2f C" % mpu.temperature)
# print("")
zitai = Update_IMU(acc_x, acc_y, acc_z, gyro_x, gyro_y, gyro_z)
# zitai = IMUupdate(acc_x, acc_y, acc_z, gyro_x, gyro_y, gyro_z) #q4
# zitai = one_filter(acc_x, acc_y, acc_z, gyro_x, gyro_y, gyro_z) #one_filter
print(zitai)
zitai = bytearray(f'pitch:{zitai[0]}, roll:{zitai[1]}, yaw:{zitai[2]}') #pitch:, roll:, yaw:
uart.write(zitai)#发送一条数据
# time.sleep(0.1)
库zitai_slove
#coding:utf-8
import math
#IMU算法更新
Kp = 100 #比例增益控制加速度计/磁强计的收敛速度
Ki = 0.002 #积分增益控制陀螺偏差的收敛速度
halfT = 0.001 #采样周期的一半
#传感器框架相对于辅助框架的四元数(初始化四元数的值)
q0 = 1
q1 = 0
q2 = 0
q3 = 0
#由Ki缩放的积分误差项(初始化)
exInt = 0
eyInt = 0
ezInt = 0
def Update_IMU(ax,ay,az,gx,gy,gz):
global q0
global q1
global q2
global q3
global exInt
global eyInt
global ezInt
# print(q0)
#测量正常化
norm = math.sqrt(ax*ax+ay*ay+az*az)
#单元化
ax = ax/norm
ay = ay/norm
az = az/norm
#估计方向的重力
vx = 2*(q1*q3 - q0*q2)
vy = 2*(q0*q1 + q2*q3)
vz = q0*q0 - q1*q1 - q2*q2 + q3*q3
#错误的领域和方向传感器测量参考方向之间的交叉乘积的总和
ex = (ay*vz - az*vy)
ey = (az*vx - ax*vz)
ez = (ax*vy - ay*vx)
#积分误差比例积分增益
exInt += ex*Ki
eyInt += ey*Ki
ezInt += ez*Ki
#调整后的陀螺仪测量
gx += Kp*ex + exInt
gy += Kp*ey + eyInt
gz += Kp*ez + ezInt
#整合四元数
q0 += (-q1*gx - q2*gy - q3*gz)*halfT
q1 += (q0*gx + q2*gz - q3*gy)*halfT
q2 += (q0*gy - q1*gz + q3*gx)*halfT
q3 += (q0*gz + q1*gy - q2*gx)*halfT
#正常化四元数
norm = math.sqrt(q0*q0 + q1*q1 + q2*q2 + q3*q3)
q0 /= norm
q1 /= norm
q2 /= norm
q3 /= norm
#获取欧拉角 pitch、roll、yaw
pitch = math.asin(-2*q1*q3+2*q0*q2)*57.3
roll = math.atan2(2*q2*q3+2*q0*q1,-2*q1*q1-2*q2*q2+1)*57.3
yaw = math.atan2(2*(q1*q2 + q0*q3),q0*q0+q1*q1-q2*q2-q3*q3)*57.3
return pitch,roll,yaw
其他的库可以看我给出的参考连接,这个库贴出来是因为我自己不知道在哪找的啦。
另外还可以使用mpu6050自带的dmp处理数据得出姿态角。这里这位大佬有做,链接如下:(演示)mpu6050+esp32 mpy,dmp姿态角运算。三行代码实现调用_哔哩哔哩_bilibili可以去群里找代码哦。这里我不贴出来了,不太好。这里我没复刻成功,原因是栈溢出了,我不知道怎么解决。😥
但是我参考了那个大佬树莓派+mpu6050的成功了,我手头刚好有个树莓派3b+就试了一下,dmp解算姿态角效果挺好的(航向角除外)。
综上,这个任务就结束了。
本帖最后由 怀66 于 2023-9-10 21:31 编辑
DTW算法录入路径的时候需要录入多少条?
引用: wangerxian 发表于 2023-9-11 09:24 DTW算法录入路径的时候需要录入多少条?
按自己需要啊,你记录了几个动作,测试时就会拿测试动作去记录的动作里找最像的,从而实现识别。所以不能记录太多动作,否者算起来会很慢。
引用: 怀66 发表于 2023-9-11 10:32 按自己需要啊,你记录了几个动作,测试时就会拿测试动作去记录的动作里找最像的,从而实现识别。所以不能 ...
一个动作只记录一次嘛?我认知里,一个动作要重复多次,才能提高识别的鲁棒性。
引用: wangerxian 发表于 2023-9-11 15:30 一个动作只记录一次嘛?我认知里,一个动作要重复多次,才能提高识别的鲁棒性。
是的是的,你所的对。原作者里一个动作是可以记录多次的,记录分数时取均值就行,但是我移植过来的时候,一开始没考虑这么多,写储存文件的代码的时候没有考虑,如果将同一个动作记录在动作名称的文件夹下即可,原作者是用uuid避免同一动作文件夹下文件重复的。另一个原因就是因为性能了,单片机性能可能真处理不了那么多序列,太慢了,加上micro python/circuitpython也比较慢的原因。具体可参考我发的链接,原作者是用树莓派做的,我自己也是现在树莓派上做了验证才移植到esp32上的。树莓派-MPU6050+DTW算法实现简单动作识别_哔哩哔哩_bilibili,算法核心层面上我都没动的,我就修改了一部分让其能跑,自己也偷懒了没重新写记录重复动作的文件,其实我也不知道如何在circuitpython/mpy里生成不会重复的名称,(不过好像random就可以用),原作者用的uuid,mpy/cpy里没有这个库我就简单弄了。