共计 7487 个字符,预计需要花费 19 分钟才能阅读完成。
钢琴是人类创作音乐的经典乐器,程序是实现创意的工具之魂。明天我给大家分享用程序实现的桌上钢琴师我的项目。本我的项目基于飞桨实现一个虚构钢琴,让大家能够在任意立体上弹奏钢琴,实现弹奏自在。
该项目标原理是利用手部关键点检测模型辨认手的关键点,获取指尖点在画面的坐标地位。当指尖关键点跨过虚构钢琴键的黄色响应线,即播放该琴键的音。
我的项目计划
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 的多过程,实现高效实时的画面显示、模型推理及后果反馈,在端侧实现较好的体验。
01 模型推理
硬件筹备—摄像头安排
- 摄像头垂直立于桌面,拍摄角度平行于桌面
因本我的项目应用的是 2D Hand Keypoint 模型,通过辨认手指在画面中的 y 方向和 x 方向的地位来判断手指尖是否有敲击动作以及敲击哪个琴键,因而,摄像头须要垂直立于桌面上,这样能最好地拍摄到敲击桌面的状况。摄像头拍摄角度程度平行于桌面,点击地位能够通过初始化校准来主动调节,这样能确保较好的交互性。
- 摄像头可选用一般 USB 网络摄像头,最好清晰度高一点
本我的项目选用无畸变摄像头,益处是无需解决畸变问题,毛病是画面拍摄的画幅不够大,无奈对应钢琴的 88 键。而选用大广角的摄像头会有畸变,尽管能够通过 OpenCV 的四角纠正校准成简直无畸变的画面,但运算量会增大,响应延时会增大。感兴趣的同学也能够尝试。
算法选型
该工作能够思考应用 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_loca…
在该项目标实际操作中,手指关键点辨认成果比拟个别,这与拍摄角度对应的训练数据比拟少无关,且无奈基于 PaddleHub 进行迁徙训练。
飞桨实现手部 21 个关键点检测模型
该模型是 ID 为“星尘局”的同学在 AI Studio 上开源的模型。我测试了一下,成果比上述计划好一些,且能够持续训练或进行迁徙训练。但其是基于 ResNet50 间接做回归,准确率和实时性还有待晋升。https://aistudio.baidu.com/aistudio/projectdetail/2235290
基于 CenterNet 的手部关键点模型
- 基于 CenterNet 模型的魔改
基于上述模型状况,本人应用飞桨框架魔改了一版 CenterNet 关键点模型,增加了基于 heatmap 辨认 landmark 的分支。本计划相似于 DeepFashion2 的冠军计划,如下图所示,DeepFashion2 的计划基于 CenterNet 上增加了 Keypoint 辨认。本计划与之相似,因为工作绝对简略,并不需要求出 bbox,因而删减了 Object size 的回归。具体代码实现将会公开在 AI Studio 我的项目。
https://aistudio.baidu.com/aistudio/projectdetail/4915699
- 手部 5 个指尖关键点
为了更好地在边缘端部署,把原来手部单手 21 个关键点简化为只训练或推理 5 个手指指尖点,缩减网络训练及推理的工夫。
- 不辨别左右手
因本我的项目利用于弹钢琴,左右手并不影响我的项目的后果,就没有辨别左手或右手。
训练数据
训练数据集来自于 Eric.Lee 的 handpose_datasets_v2 数据集,在 handpose_datasets_v1 的根底上减少了左右手属性 ”handType”: “Left” or “Right”,数据总量为 38w+。
https://aistudio.baidu.com/aistudio/datasetdetail/162171/0
程序运行流程
程序整体应用生产者 - 消费者模式,分为三个模块:输出模块、手部关键点预测模块、主显示及 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 = False
exit()
elif event.type == pygame.MOUSEBUTTONUP:
thresholdY=resetKeyboardPos(ftR,thresholdY)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
prodecer.runFlag = False
exit()
elif event.key == pygame.K_SPACE:
thresholdY=resetKeyboardPos(ftR,thresholdY)
return thresholdYdefloopRun(dataQueue,wSize,hSize,prodecer,consumer,thresholdY,movieDict,skipFrame):
# tip position of hand down
ftDown1={}
ftDown2={}
# tip position for now
ft1={}
ft2={}
# tip position of hand up
ftUp1={}
ftUp2={}
#
stageR=-1
stageL=-1
resXR=-1
resXL=-1
idsR=-1
idsL=-1
#
biasDict1={}
biasDict2={}
screen = pygame.display.set_mode((wSize,hSize))
# cap = cv2.VideoCapture(path)
num=-1
keyNums=12
biasy=20
result={}
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 result
result={}
if __name__=='__main__':
link=0
wSize=640
hSize=480
skipFrmae=2
dataQueue = Queue(maxsize=2)
resultDeque = Queue()
thresholdY=250
producer = 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)
02 部署
下载模型代码
下载本我的项目数据集的 Piano.zip 压缩包(或 data/data181662/ 文件夹中的压缩包 ) 到本地并解压,能够依据不同具体情况抉择对应的版本开始部署。
X86 / X64 零碎上运行
- 装置飞桨及 pygame 等所需的库;
- 解压 data 中的压缩包到目录中;
- 进入 HandPiano 文件夹,运行 python main.py 程序即可。
在 ARM 设施,如 Jetson NX 上运行
- 从 0 开始
如果从 0 开始部署飞桨到 Jetson NX 可参考“ゞ灰酱”的我的项目:https://aistudio.baidu.com/aistudio/projectdetail/969585?chan…
- 下载 Paddle Inference 库并装置
https://www.paddlepaddle.org.cn/inference/v2.4/guides/install…
- 解压 data 中的压缩包到目录中
- 进入 HandPiano 文件夹,python main.py 运行程序即可
03 运行
按硬件配置设置好,按 C 部署,启动程序,看到如下 UI。
- 成果展现
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。
一起开始弹钢琴吧!