之前是做的yolov3手势物体识别,最近几天我将该项目进行了重新的整理和升级,实现了yolov5手势物体识别,同时为了方便更多的人直接拿来应用,我生成了支持windows系统的应用小程序,即便你电脑上没有安装pytorch,没有安装cuda、python,都可以使用~!
相关资料:
yolov3手势物体识别
应用程序效果如下:
yolov5手势物体识别
技术说明
本项目使用了三个算法模型进行的功能实现。yolov5做手部目标检测,ReXNet(支持Resnet系列)做手部21关键点回归检测,Resnet50做物体分类识别。(其实就是三个算法做的级联)
yolov5手部目标检测
使用yolov5s进行训练,数据集共有3W+,因本地训练环境受限,我只训练到mAP 64%左右,因此准确率并不是很高,大家有需要的可以自行再去训练~
数据集说明
数据集链接:
(ps:这里的数据集采用的公共数据集,没有做过数据清洗)
链接:https://pan.baidu.com/s/1jnXH3yBuGJ8_DRXu-gKtNg
提取码:yypn
数据集格式:
images:存放所有的数据集
labels:已经归一化后的label信息
train.txt:训练集划分,25934张
val.txt:验证集划分,3241张
test.txt:测试集划分,3242张图
训练实验记录
采用马赛克数据增强,如下图:
评价指标:
(我这里只训练了大该十多个epoch,没服务器训练太慢了~(T^T)~,所以准确率比较低,有需要的可以自己训练一下)
P | R | mAP 0.5 | mAP 0.5:0.95 |
0.75396 | 0.59075 | 0.64671 | 0.27652 |
手部21关键点检测
手部关键点识别采用的网络为ReXNet(支持Resnet系列),这里需要说明的是关键点预测并没有采用openpose网络!而是采用的坐标回归方法,这个问题需要强调一下,不然总有小伙伴问我,而且还很质疑~ 在本任务中,由于有yolo作为前置滤波器算法将手部和背景进行分离,分离后的图像前景和背景相对平衡,而且前景(手部)占主要部分,因此任务其实相对简单,可以采用坐标回归方法。
网络的定义在yolov5_hand_pose/components/hand_keypoints/models/。
21个关键点,那就是有42个坐标(x,y坐标)。因此代码中num_classes=42,代码:
class handpose_x_model(object):
def __init__(self,
model_path = '',
img_size= 256,
num_classes = 42,# 手部关键点个数 * 2 : 21*2
model_arch = "rexnetv1"
):
再看下网络最终的输出:
这里输出的通道数为num_classes!所以和openpose是有一定区别的!这里必须要说明一下,不然老有人不信哈哈~
features.append(nn.AdaptiveAvgPool2d(1))
self.features = nn.Sequential(*features)
self.output = nn.Sequential(
nn.Dropout(dropout_factor),
nn.Conv2d(pen_channels, num_classes, 1, bias=True))
def forward(self, x):
x = self.features(x)
x = self.output(x).squeeze()
return x
言归正传,21关键点识别效果如下,首先是通过yolov5提取手部box信息,再把手分离出来,将手部图像resize为256x256大小,用关键点网络进行关键点回归。
训练
训练代码在train.py中。
可采用提供的预权重进行fine tune训练。
输入以下命令开始训练:
python train.py --model resnet_50 --train_path [数据集路径] --fintune_model 【fine tune模型路径】--batch_size 16
如果是fine tune训练,建议初始学习率(init_lr)设置为5e-4,否则建议设置为1e-3。
损失函数此次采用的是MSE,还可支持wing loss。
训练好的权重会保存在model_exp中,对应的tensorboard会存储在logs中
分类网络
这里的分类网络采用是resnet50网络,权重为ImageNet数据集上的(1000个类),可以根据自己任务需求去训练。
网络定义在yolov5_hand_pose/components/classify_imagenet/models。
那具体是如何分类的呢?
首先触发分类模型的手势是食指和大拇指捏合。主要是计算两个关键点的欧式距离,当距离小于阈值则为触发状态click_state=True:
# 计算食指大拇指的距离
dst = np.sqrt(np.square(thumb_[0]-index_[0]) +np.square(thumb_[1]-index_[1]))
# 计算大拇指和手指相对手掌根部的角度:
angle_ = vector_2d_angle((thumb_[0]-hand_root_[0],thumb_[1]-hand_root_[1]),(index_[0]-hand_root_[0],index_[1]-hand_root_[1]))
# 判断手的点击click状态,即大拇指和食指是否捏合
click_state = False
if dst<dst_thr and angle_<angle_thr: # 食指和大拇指的坐标欧氏距离,以及相对手掌根部的相对角度,两个约束关系判断是否点击
click_state = True
那么又有个问题了,总不能我两个手指一捏合就触发,这带来了很大的误触,这里解决的方法是判断触发状态(可以理解为时间长短)。代码如下:
'''
判断各手的click状态是否稳定(点击稳定充电环),即click是否持续一定阈值
注意:charge_cycle_step 充电步长越大,触发时间越短
'''
def judge_click_stabel(img,handpose_list,charge_cycle_step = 32):
flag_click_stable = True
for i in range(len(handpose_list)):
_,_,_,dict_ = handpose_list[i]
id_ = dict_["id"]
click_cnt_ = dict_["click_cnt"]
pt_ = dict_["choose_pt"]
if click_cnt_ > 0:
# print("double_en_pts --->>> id : {}, click_cnt : <{}> , pt : {}".format(id_,click_cnt_,pt_))
# 绘制稳定充电环
# 充电环时间控制
charge_cycle_step = charge_cycle_step # 充电步长越大,触发时间越短
fill_cnt = int(click_cnt_*charge_cycle_step)
if fill_cnt < 360:
cv2.ellipse(img,pt_,(16,16),0,0,fill_cnt,(255,255,0),2)
else:
cv2.ellipse(img,pt_,(16,16),0,0,fill_cnt,(0,150,255),4)
# 充电环未充满,置为 False
if fill_cnt<360:
flag_click_stable = False
else:
flag_click_stable = False
return flag_click_stable
当两个手都触发捏合动作,那么判断是有效动作,同时将左右手选定的区域截出来(和yolo的操作类似),送入分类网络进行分类识别。
语音播报
当手势动作触发成功后会触发语音播报函数,此时会自动语音播放"正在识别物体请等待",如果成功识别,并且也有该物体的语音包(需要自己录制),那么会说“您识别的物体为。。。”
语音播报函数:
'''
启动识别语音进程 该项目中主要用到下面的自定义函数
'''
def audio_process_recognize_up_edge(info_dict):
while (info_dict["handpose_procss_ready"] == False): # 等待 模型加载
time.sleep(2)
gesture_names = ["double_en_pts"] # 姿态列表,
gesture_dict = {}
for k_ in gesture_names:#k_= double_en_pts
gesture_dict[k_] = None #gesture_dict[double_en_pts]=None
while True:
time.sleep(0.01)
# print(" --->>> audio_process")
try:
for g_ in gesture_names: # 输出 double_en_pts ,因为gesture_name列表内容为str,所以g_也是str,输出的g_=double_en_pts
if gesture_dict[g_] is None: # gesture_dict[double_en_pts] 为真
gesture_dict[g_] = info_dict[g_] #info_dict[g_]为False
else:
if ("double_en_pts"==g_):
if (info_dict[g_]^gesture_dict[g_]) and info_dict[g_]==True:# 判断Click手势信号为上升沿,Click动作开始
playsound("./materials/audio/sentences/IdentifyingObjectsWait.mp3")
playsound("./materials/audio/sentences/ObjectMayBeIdentified.mp3")
if info_dict["reco_msg"] is not None:
print("process - (audio_process_recognize_up_edge) reco_msg : {} ".format(info_dict["reco_msg"]))
doc_name = info_dict["reco_msg"]["label_msg"]["doc_name"]
reco_audio_file = "./materials/audio/imagenet_2012/{}.mp3".format(doc_name)
if os.access(reco_audio_file,os.F_OK):# 判断语音文件是否存在
playsound(reco_audio_file)
info_dict["reco_msg"] = None
gesture_dict[g_] = info_dict[g_]
except Exception as inst:
print(type(inst),inst) # exception instance
if info_dict["break"] == True:
break
整个项目,也就是三个算法、语音播报等是采用多进程显现的,四个功能之间的通信是利用的共享字典进行的数据共享。