★★★ 本文源自AI Studio社区精品项目,【点击此处】查看更多精品内容 >>>
项目背景
元宇宙是利用科技手段进行链接与创造的,与现实世界映射与交互的虚拟世界,具备新型社会体系的数字生活空间。目前,元宇宙刚处于起步阶段,而市场上与元宇宙教育相关的产品服务也仍处于待开发的状态中。元宇宙的身临其境式体验能够让数字交互更加人性化,而与教育融合已被认为是“元宇宙”最重要、可行的应用场景之一。网络学习面临的最紧迫问题之一——功效性问题,如今的网络学习基本是以视频会议的形式展开的,但这种形式的学习往往很难达到或接近于面对面教学的水平。而在元宇宙的互动中,我们会感到自己正在经历真实的体验并创造真实的记忆。
传统的线上授课,如钉钉、腾讯会议等形式,老师授课形式单一,缺少肢体动作,与学生之间缺乏互动,这使得线上学习不能还原坐在教室上课的氛围,学生往往积极性不高。通常情况下,人们通过手的接触或手的动作可以解读出对方的心理活动或心理状态,同时还可将自己的意图传达给对方。因为手势动作可以更加生动、鲜明地表达人类的思想。即使是在教师资格证面试的时候,手势也是重要的一个考察方面。研究表明,教师的指点手势能提升学习表现,精彩的课程需要适当地运用手势语言。
我们已基于希壤开发者工具在Unity中一比一复刻了郑州大学校园特色场景,其中有会议室、教室、图书馆等工作学习场所,并在会议室与教室这两处场景中也已经实现了多种交互功能,其中包括在元宇宙中实现在线视频投屏、实时直播、问答交互等等功能,让元宇宙在线课堂从概念中走出变为现实。
有此基础,为了让元宇宙在线课堂授课、上课的用户交互体验更加真实,该作品将基于CNN的NLP算法、云计算交互技术、Unity人物交互技术等应用于元宇宙教育,开发元宇宙在线课堂教学的手势同步系统。本项目的独创性在于:
- 目前市场上的动作捕捉设备价格在上万甚至上百万,而本项目基于惯性传感器开发的手势同步系统,能够以较低的成本实现人物的手势动作捕捉。
- 产品小巧便捷,极易操作使用。用户仅需在手腕上佩戴本产品,在联网的情况下即可在我们所开发的一系列元宇宙场景中实时实现手势动作的同步。
设计目标
本项目开发的动作识别系统是为了在教师佩戴手部惯性传感器后实时识别教师的动作,并通过与Unity的连接使元宇宙校园中的数字人物做出相同的动作。旨在为无法到线下传统教室进行教学的教师和同学提供更为真实的互动体验。后续将封装为手环并接入蓝牙模块,用户佩戴后联网即可使用。
本项目目前实现了在虚拟场景中授课的功能,该功能与现实场景联系紧密,而借助百度飞桨实现的AI手势同步技术使得手势可与虚拟人物同步实时的联系起来,不仅可以用于已有的校园虚拟场景,对老师授课、在线上展会、人物互动等场景也有很大的帮助。
技术方案
1. 搭建元宇宙课堂
人物可以从所搭建的教学楼进入课堂,学生点击座位即可落座,老师可以登录OBS客户端连接元宇宙课堂端口,通过声画同传功能和直播投屏方式进行授课。
2. 构建模型
通过手戴惯性传感器采集数据,经处理形成训练数据集,利用CNN算法进行动作识别,不断改进得到效果较优的模型;
(1)采集数据
佩戴手部惯性传感器共采集样本7200份,包括摊手、向旁指、指向学生、向后指、上课中手臂摆动、挥手六种教师上课时的常用动作。
import keyboard
import serial
def get_data(save_path):
# 准备一个文件,保存数据
with open(save_path,'w+') as f:
# 创建一个列表用于存储全部数据
all_list = list()
# 配置串口名称、比特率、超时时间
port = 'COM3'
bps = 115200
timex = 0.005
# 按下a键开始读取数据
print('按下a键开始读取数据')
keyboard.wait('a')
print('读取数据中......按下q键停止读取数据')
# 实例化并连接串口
ser = serial.Serial(port, bps, timeout=timex)
while True:
# 按下q键停止读取数据
if keyboard.is_pressed('q'):
ser.close()
print("数据读取完成")
print('***************')
break
else:
data = ser.read_all()
a_list = list(data)
all_list.append(a_list)
# 将全部数据分为23字节一行写入txt文件中
for a in all_list:
for x in range(0,len(a)):
if a[x] == 127 :
if x == len(a)-1:
f.write(str(a[x])+'\n')
else:
for y in range(x+1,x+2):
if a[y]==128 :
f.write('\n'+str(a[x])+'\t')
else:
f.write(str(a[x])+'\t')
else:
f.write(str(a[x])+'\t')
f.close()
(2)数据预处理
将采集到的传感器数据由十六进制转换为十进制数据,并将数据进行归一化处理。为使数据序列长度一致,使用padding函数填充序列长度。最后将数据打乱顺序全部整合在一个csv文件中。
def Normalization_9(trans_path,result_path,label):
# 传感器参数
Accel_Scale = 20
Rate_Scale = 1260
Angle_Scale = 360
Sensor_Scale = 65536
# 准备一个文件,保存转换数据
with open(result_path,'w+') as f2:
# 写入表头
f2.write('x0'+','+'x1'+','+'x2'+','+'x3'+','+'x4'+','+'x5'+','+'x6'+','+'x7'+','+'x8'+','+'cls'+'\n')
# 打开需转化的txt文件
with open(trans_path,'r') as f1:
lines = f1.readlines()
for i in lines:
sensors = [0]*9
all_data=list()
l = i.split()
# 中间计算
sensors[0] = two_ten(ten_two(l[3])+ten_two(l[2]))
sensors[1] = two_ten(ten_two(l[5])+ten_two(l[4]))
sensors[2] = two_ten(ten_two(l[7])+ten_two(l[6]))
sensors[3] = two_ten(ten_two(l[9])+ten_two(l[8]))
sensors[4] = two_ten(ten_two(l[11])+ten_two(l[10]))
sensors[5] = two_ten(ten_two(l[13])+ten_two(l[12]))
sensors[6] = two_ten(ten_two(l[15])+ten_two(l[14]))
sensors[7] = two_ten(ten_two(l[17])+ten_two(l[16]))
sensors[8] = two_ten(ten_two(l[19])+ten_two(l[18]))
Ax = ((sensors[0] * Accel_Scale / Sensor_Scale) + (Accel_Scale/2)) / Accel_Scale
Ay = ((sensors[1] * Accel_Scale / Sensor_Scale) + (Accel_Scale/2)) / Accel_Scale
Az = ((sensors[2] * Accel_Scale / Sensor_Scale) + (Accel_Scale/2)) / Accel_Scale
Gx = ((sensors[3] * Rate_Scale / Sensor_Scale) + (Rate_Scale/2)) / Rate_Scale
Gy = ((sensors[4] * Rate_Scale / Sensor_Scale) + (Rate_Scale/2)) / Rate_Scale
Gz = ((sensors[5] * Rate_Scale / Sensor_Scale) + (Rate_Scale/2)) / Rate_Scale
R = ((sensors[6] * Angle_Scale / Sensor_Scale) + (Angle_Scale/2)) / Angle_Scale
P = ((sensors[7] * Angle_Scale / Sensor_Scale) + (Angle_Scale/2)) / Angle_Scale
Y = ((sensors[8] * Angle_Scale / Sensor_Scale) + (Angle_Scale/2)) / Angle_Scale
# 保留九位小数
a=('%.9f' % Ax)
b=('%.9f' % Ay)
c=('%.9f' % Az)
d=('%.9f' % Gx)
e=('%.9f' % Gy)
f=('%.9f' % Gz)
g=('%.9f' % R)
h=('%.9f' % P)
j=('%.9f' % Y)
cls_action=int(label)
all_data.append(a)
all_data.append(b)
all_data.append(c)
all_data.append(d)
all_data.append(e)
all_data.append(f)
all_data.append(g)
all_data.append(h)
all_data.append(j)
all_data.append(cls_action)
# 判断各数值是否在对应区间内并转换写入txt文件
for x in all_data[0:9]:
x = float(x)
f2.write(str(x) +',')
z = all_data[9]
f2.write(str(z))
f2.write('\n')
f1.close() # 关闭文件
f2.close()
(3)模型训练与优化
本作品搭建了基于CNN的动作识别模型。当数据传入输入层后,经过两个卷积层处理,后依次进行Dropout与Maxpool操作,最终经过两个全连接层后通过一个Logsoftmax激活层输出各类手势动作的概率分布。经过卷积层的处理,提取的特征进入池化层进行降采样以减少后续数据的处理量,将提取的数据特征拉伸为特征向量,再经过若干全连接层后,实现数据特征的较好分类识别。
if args.mode == 'train':
# 设置训练模式
model.train()
# 设置loss
loss = paddle.nn.NLLLoss()
# 设置optimizer和scheduler
optim = paddle.optimizer.Adam(parameters=model.parameters(),
learning_rate=config.get('lr'),
epsilon=config.get('eps'),
weight_decay=config.get('weight_decay'))
scheduler = paddle.optimizer.lr.StepDecay( learning_rate=config.get('lr'),
step_size=config.get('lr_scheduler_step_size'),
gamma=config.get('lr_scheduler_gamma'))
# 设置数据集和数据加载器
logging.info("Start train data preparation")
window_shift = config.get("window_shift")
window_size = config.get("window_size")
input_size = config.get("input_dim")
dataset = IMUDataset(args.imu_dataset_file, window_size, input_size, window_shift)
loader_params = {'batch_size': config.get('batch_size'),
'shuffle': True,
'num_workers': config.get('n_workers')}
dataloader = paddle.io.DataLoader(dataset, **loader_params)
logging.info("Data preparation completed")
# 获取训练详细信息
n_freq_print = config.get("n_freq_print")
n_freq_checkpoint = config.get("n_freq_checkpoint")
n_epochs = config.get("n_epochs")
# 训练过程
checkpoint_prefix = join(utils.create_output_dir('out'),utils.get_stamp_from_log())
n_total_samples = 0.0
loss_vals = []
sample_count = []
logging.info("Start training")
for epoch in range(n_epochs):
for batch_idx, minibatch in enumerate(dataloader):
y = 0
minibatch["imu"] = paddle.to_tensor(minibatch["imu"],dtype='float32')
label = minibatch.get('label')
batch_size = label.shape[0]
n_total_samples += batch_size
# 梯度清零
optim.clear_grad()
# 前向传播
res = model(minibatch)
# 计算loss
criterion = loss(res, label)
# 收集用于记录和绘图的信息
batch_loss = criterion.item()
loss_vals.append(batch_loss)
sample_count.append(n_total_samples)
# 反向传播
criterion.backward()
optim.step()
# 记录训练集上的loss
if batch_idx % n_freq_print == 0:
logging.info("[Batch-{}/Epoch-{}] batch loss: {:.3f}".format(
batch_idx+1, epoch+1,
batch_loss))
# 保存checkpoint
if (epoch % n_freq_checkpoint) == 0 and epoch > 0:
paddle.save(model.state_dict(), checkpoint_prefix + '_checkpoint-{}.pdparams'.format(epoch))
paddle.save(optim.state_dict(), checkpoint_prefix + '_checkpoint-{}.pdopt'.format(epoch))
# Scheduler更新
scheduler.step()
logging.info('Training completed')
paddle.save(model.state_dict(), checkpoint_prefix + '_final.pdparams')
paddle.save(optim.state_dict(), checkpoint_prefix + '_final.pdopt')
3. 部署在云服务器
将模型部署在云服务器,用户手腕佩戴传感器将手部动作数据传输到云端,运行动作识别算法模型,将推理结果输出到Unity中。
4. 虚拟人物做出对应手势
在Unity中使用C#脚本调用云端模型,通过内置骨骼动作系统将自主设计的教师常用手势动作绑定到元宇宙中的教师虚拟人物上,将动作在已有虚拟人物身上将识别的手势动作体现出来。
效果展示
当用户做出动作时,传感器数据传入云端,Unity中的教室人物做出对应动作。
总结
本项目借助成熟的百度深度学习框架PaddlePaddle,稳健地搭建了动作识别模型。实际应用中,能快速完成三维空间中手势识别任务,教师只需在手部佩戴简易的惯性传感器装置,元宇宙教室中的人物即可做出相应的抬手、挥手等动作,实现课堂“动起来”的效果。
本团队的元宇宙虚拟校园项目已经与百度希壤展开合作,本项目所建立的元宇宙课堂将作为其中的一部分部署在希壤中。与百度希壤合作将作为推广本项目的强有力支持,希壤用户可以亲身体验本项目的效果,同时我们将与百度共同通过线上自媒体、线下开发者大会等方式进行宣传,我们也会积极寻求与各大校园的合作,使项目得到进一步的推广。