热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

没有钢琴也可实现弹奏自由?实时在Jetson上运行单阶段手指关键点模型

钢琴是人类创作音乐的经典乐器,程序是实现创意的工具之魂。今天我给大家分享用程序实现的桌上钢琴师项目。本项目基于飞桨实现一个虚拟钢琴,让大家可以在任意平面

钢琴是人类创作音乐的经典乐器,程序是实现创意的工具之魂。今天我给大家分享用程序实现的桌上钢琴师项目。本项目基于飞桨实现一个虚拟钢琴,让大家可以在任意平面上弹奏钢琴,实现弹奏自由。

bd5c8006cb69b36decaaf0dc468ce773.png

该项目的原理是利用手部关键点检测模型识别手的关键点,获取指尖点在画面的坐标位置。当指尖关键点跨过虚拟钢琴键的黄色响应线,即播放该琴键的音。

835351dab060223ca3f3a9633b066834.jpeg

项目方案

1)使用摄像头获取桌面上手指的实时画面;

2)手指关键点模型识别画面中手指指尖的x方向与y方向坐标;

3)比较当前每个手指指尖的y方向坐标与校准时的y方向坐标,判定某个手指尖是否做了敲击动作;

4)根据敲击动作的指尖的x方向坐标来判定具体按了哪个键;

5)通过pygame的UI显示按键效果,并播放对应的琴键音。

项目特点

1)基于飞桨实现轻量化的单阶段关键点识别模型。由于边缘设备算力紧张,为此,在运行实时手指关键点识别模型时,将手指识别简化为只识别5个指尖,不区分左右手。

https://aistudio.baidu.com/aistudio/projectdetail/4915699

2)用pygame作为主UI框架。实时显示摄像头的画面及提示UI,画面中拍到的手指在桌面上敲击,即用户敲击虚拟琴键时,立即播放对应钢琴音。

3)使用生产者-消费者模式,充分利用Python的多进程,实现高效实时的画面显示、模型推理及结果反馈,在端侧实现较好的体验。

00eb3497223dbb088ece4a8e155e7ee1.png模型推理

8659c602ab9b92d41c66a84f53aea078.png硬件准备—摄像头布置

  • 摄像头垂直立于桌面,拍摄角度平行于桌面

因本项目使用的是2D Hand Keypoint模型,通过识别手指在画面中的y方向和x方向的位置来判断手指尖是否有敲击动作以及敲击哪个琴键,因此,摄像头需要垂直立于桌面上,这样能最好地拍摄到敲击桌面的情况。摄像头拍摄角度水平平行于桌面,点击位置可以通过初始化校准来自动调节,这样能确保较好的交互性。 

de50dde9b403ad41e196c7b38c61aea8.png

  • 摄像头可选用普通USB网络摄像头,最好清晰度高一点

本项目选用无畸变摄像头,好处是无需处理畸变问题,缺点是画面拍摄的画幅不够大,无法对应钢琴的88键。而选用大广角的摄像头会有畸变,虽然可以通过OpenCV的四角纠正校准成几乎无畸变的画面,但运算量会增大,响应延时会增大。感兴趣的同学也可以尝试。

56c1e43360d305cf16ec67addbc6d6e3.png算法选型

该任务可以考虑使用3D Hand Keypoint Detection算法或2D Hand Keypoint Detection算法。如果考虑摄像头空间位置及角度,可考虑用3D Hand Keypoint Detection算法计算指尖三维空间位置,精度应该更高,受摄像头角度影响更小。使用2D Hand Keypoint Detection算法则需要通过固定摄像头位置及角度,才能实现同样的功能。

因本项目需要重点考虑算力问题,因此使用了2D Hand Keypoint Detection算法。基于2D Hand Keypoint Detection的算法也有很多。PaddleHub中本身就已集成了手部姿态模型,AI Studio上也有大佬放出了基于ResNet50直接回归手部关键点的项目。在多次尝试后,感觉速度与精确度还有提升的空间,因此我使用飞桨框架,基于CenterNet魔改了一版手指关键点模型。

下面对上述3个模型进行简单介绍。

hand_pose_localization模型

模型源自CMU的OpenPose开源项目,目前已经集成到PaddleHub中。

https://www.paddlepaddle.org.cn/hubdetail?name=hand_pose_localization&en_category=KeyPointDetection

在该项目的实际操作中,手指关键点识别效果比较一般,这与拍摄角度对应的训练数据比较少有关,且无法基于PaddleHub进行迁移训练。

飞桨实现手部21个关键点检测模型

该模型是ID为“星尘局”的同学在AI Studio上开源的模型。我测试了一下,效果比上述方案好一些,且可以继续训练或进行迁移训练。但其是基于ResNet50直接做回归,准确率和实时性还有待提升。

https://aistudio.baidu.com/aistudio/projectdetail/2235290

b541bc25223206ebc0ecf2feef89327e.png

基于CenterNet的手部关键点模型

  • 基于CenterNet模型的魔改

基于上述模型情况,自己使用飞桨框架魔改了一版CenterNet关键点模型,添加了基于heatmap识别landmark的分支。本方案类似于DeepFashion2的冠军方案,如下图所示,DeepFashion2的方案基于CenterNet上添加了Keypoint识别。本方案与之类似,由于任务相对简单,并不需要求出bbox,因此删减了Object size的回归。具体代码实现将会公开在AI Studio项目。

https://aistudio.baidu.com/aistudio/projectdetail/4915699

d19edd444d531d297db60236d931b21f.jpeg

  •  手部5个指尖关键点

为了更好地在边缘端部署,把原来手部单手21个关键点简化为只训练或推理5个手指指尖点,缩减网络训练及推理的时间。

  • 不区分左右手

因本项目应用于弹钢琴,左右手并不影响项目的结果,就没有区分左手或右手。

a60ec3f26757ab8fc67e14735073b3c0.png训练数据

训练数据集来自于Eric.Lee的handpose_datasets_v2数据集,在handpose_datasets_v1的基础上增加了左右手属性"handType": "Left" or "Right",数据总量为38w+。

https://aistudio.baidu.com/aistudio/datasetdetail/162171/0

cbe372ba4127ae90af22a2e82daa7dc3.png程序运行流程

程序整体使用生产者-消费者模式,分为三个模块:输入模块、手部关键点预测模块、主显示及UI处理按键响应模块。

输入模块放在子进程是“生产者”,输入的图片加入到可跨进程读写的queue中,给到主进程的消费者。“消费者”包含关键点预测模块,预测手指关键点结果与画面及UI进行叠加,通过pygame来显示。

程序采用多进程处理,输入图片是一个进程,模型推理与UI响应是一个进程,能更高效运行,避免出现卡顿。

输入模块(生产者)

使用OpenCV的cv2.videoCapture读取视频流或摄像头画面或视频。获得的画面放入dataQueue中等待处理,获得画面frame,添加到dataQueue中。

手部关键点预测模块(消费者)

把pygame作为呈现端,摄像头画面、叠加的UI或提示、按键响应均通过pygame实现。

主UI模块

本项目使用pygame作为UI呈现端。pygame播放声音更灵活,可同时播放多个声音。 

import pygamefrom pygame.localsimport *from sys import exitimport sys
pathDict={}
pathDict['hand']='../HandKeypoints/'for path in pathDict.values():sys.path.append(path)import cv2import timefrom collections import dequefrom PIL importImageimport tracebackfrom multiprocessing import Queue,Processfrom ModuleSound import effectDict# from ModuleHand import handKeypointsimport CVTools as CVTimport GameTools as GTfrom ModuleConsumer import FrameConsumer
from predict7 import CenterNetfrom ModuleInput import  FrameProducerimport numpy as np
pygame.init()
defframeShow(frame,screen):#
# timeStamp = cap.get(cv2.CAP_PROP_POS_MSEC)
# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)frame=np.array(frame)[:,:,::-1]
#print('frame',frame.shape)frame = cv2.transpose(frame)frame = pygame.surfarray.make_surface(frame)screen.blit(frame, (0, 0))pygame.display.update()
# return timeStamp
defresetKeyboardPos(ftR,thresholdY):print('key SPACE',ftR)
iflen(ftR)>0:ftR=np.array(ftR)avrR=np.average(ftR[:,1])thresholdY=int(avrR)print('reset thresholdY',thresholdY)
return thresholdY
defkeyboardResponse(prodecer,ftR,thresholdY):for event in pygame.event.get():
if event.type == pygame.QUIT:prodecer.runFlag = Falseexit()
elif event.type == pygame.MOUSEBUTTONUP:thresholdY=resetKeyboardPos(ftR,thresholdY)elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:prodecer.runFlag = Falseexit()                
elif event.key == pygame.K_SPACE:thresholdY=resetKeyboardPos(ftR,thresholdY)
return thresholdYdefloopRun(dataQueue,wSize,hSize,prodecer,consumer,thresholdY,movieDict,skipFrame):
# tip position of hand downftDown1={}ftDown2={}
# tip position for nowft1={}ft2={}
# tip position of hand upftUp1={}ftUp2={}
#stageR=-1stageL=-1resXR=-1resXL=-1idsR=-1idsL=-1
#biasDict1={}biasDict2={}screen = pygame.display.set_mode((wSize,hSize))
# cap = cv2.VideoCapture(path)num=-1keyNums=12biasy=20result={}
whileTrue:
##FPS=prodecer.fps/skipFrame
if FPS >0:videoFlag = True
else:videoFlag = False
####
# print('ppp', len(dataDeque), len(result))
if  dataQueue.qsize()==0 :time.sleep(0.1)
continue
# print('FPS',FPS)elif dataQueue.qsize()>0:
##image=dataQueue.get()
## flir left right:image=image[:,::-1,:]result=consumer.process(image,thresholdY)resImage=result['image']ftR=result['fringerTip1']ftL=result['fringerTip2']
#print('resImage',resImage.size)thresholdY=keyboardResponse(prodecer, ftR,thresholdY)if videoFlag:num += 1
if num == 0:T0 = time.time()print('T0',T0,num*(1./FPS))try:           resImage = GT.uiProcess(resImage,ftR,ftL,biasy)except Exception as e:traceback.print_exc()try:fringerR,keyIndexR,stageR=GT.pressDetect(ftR,stageR,thresholdY,biasy,wSize,keyNums)fringerL,keyIndexL,stageL=GT.pressDetect(ftL,stageL,thresholdY,biasy,wSize,keyNums)#print('resR',idsR,resR,idsL,resL)
except Exception as e:traceback.print_exc()
#GT.soundPlay(effectDict,keyIndexR)GT.soundPlay(effectDict,keyIndexL)
#resImage=GT.moviePlay(movieDict,keyIndexR,resImage,thresholdY)resImage=GT.moviePlay(movieDict,keyIndexL,resImage,thresholdY)frameShow(resImage, screen)
#clear resultresult={}if __name__=='__main__':link=0wSize=640hSize=480skipFrmae=2dataQueue = Queue(maxsize=2)resultDeque = Queue()thresholdY=250producer = FrameProducer(dataQueue, link)
##frOntPIL=Image.open('pianoPic/pianobg.png')handkeypoint=CenterNet(folderPath='/home/sig/sig_dir/program/HandKeypoints/')cOnsumer=FrameConsumer(dataQueue,resultDeque,handkeypoint,frontPIL)producer.start()
#moviePicPath='pianoPic/'movieDict=GT.loadMovieDict(moviePicPath)
#loopRun(dataQueue, wSize, hSize, producer,consumer,thresholdY,movieDict,skipFrmae)

27cd1d3b7393b9ae0aa7be9ee0ce4d56.png部署

8d6ab34cf8ad32915ba8bb0a48091548.png下载模型代码

下载本项目数据集的Piano.zip压缩包(或data/data181662/文件夹中的压缩包) 到本地并解压,可以根据不同具体情况选择对应的版本开始部署。

75e4b0832a08f8cb633ebb709d651d0d.pngX86 / X64系统上运行

  • 安装飞桨及pygame等所需的库;

  • 解压data中的压缩包到目录中;

  • 进入HandPiano文件夹,运行python main.py程序即可。

0a8b600bd85b8454e9d1ceb804421495.png在ARM设备,如Jetson NX上运行

  • 从0开始

如果从0开始部署飞桨到Jetson NX可参考“ゞ灰酱”的项目:

https://aistudio.baidu.com/aistudio/projectdetail/969585?channelType=0&channel=0

  • 下载 Paddle Inference库并安装

https://www.paddlepaddle.org.cn/inference/v2.4/guides/install/download_lib.html

  • 解压data中的压缩包到目录中

  • 进入 HandPiano 文件夹,python main.py运行程序即可

e0e8525da59245d69b3cbd6f7dcdb711.png运行

按硬件配置设置好,按C部署,启动程序,看到如下UI。

dfff6627660b7f21db8fc441b512c2ff.jpeg

  • 效果展示

https://www.bilibili.com/video/BV1wT411g7MU/

  • 一只手的五指放于桌面上,当5个手指点的圆形都出现后,点击鼠标左键进行“点击位置校准”。

  • “点击位置校准”后,会调整琴键UI位置,黄线会在指尖所成的直线处。

  • 当指尖的点越过黄线进入琴键位置后即触发该琴键的声音。

  • 标示有C1的就表示C大调的do,之后的2、3、4、5、6、7 就是对应C大调的简谱的 2(re)、3(mi)、 4(fa)、 5(sol)、 6(la)、 7(si)。C1左边的是降一个调的简谱的5、6、7。

    一起开始弹钢琴吧!

关注【飞桨PaddlePaddle】公众号

获取更多技术内容~



推荐阅读
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Java实战之电影在线观看系统的实现
    本文介绍了Java实战之电影在线观看系统的实现过程。首先对项目进行了简述,然后展示了系统的效果图。接着介绍了系统的核心代码,包括后台用户管理控制器、电影管理控制器和前台电影控制器。最后对项目的环境配置和使用的技术进行了说明,包括JSP、Spring、SpringMVC、MyBatis、html、css、JavaScript、JQuery、Ajax、layui和maven等。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 本文详细介绍了在ASP.NET中获取插入记录的ID的几种方法,包括使用SCOPE_IDENTITY()和IDENT_CURRENT()函数,以及通过ExecuteReader方法执行SQL语句获取ID的步骤。同时,还提供了使用这些方法的示例代码和注意事项。对于需要获取表中最后一个插入操作所产生的ID或马上使用刚插入的新记录ID的开发者来说,本文提供了一些有用的技巧和建议。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • FeatureRequestIsyourfeaturerequestrelatedtoaproblem?Please ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文讨论了一个数列求和问题,该数列按照一定规律生成。通过观察数列的规律,我们可以得出求解该问题的算法。具体算法为计算前n项i*f[i]的和,其中f[i]表示数列中有i个数字。根据参考的思路,我们可以将算法的时间复杂度控制在O(n),即计算到5e5即可满足1e9的要求。 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
author-avatar
礼貌粑粑
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有