手势识别(一) - 项目概述与简单应用介绍

我公司的科室开始在公众号上规划一些对外的技术文章了,包括实战项目、模型优化、端侧部署和一些深度学习任务基础知识,而我负责人体图象相关技术这一系列文章,偶尔也会出一些应用/代码解读等相关的文章。
文章在同步发布至公众号和博客,顺带做一波宣传。有兴趣的还可以扫码加入我们的群。
(文章有写的不好的地方请见谅,另外有啥错误的地方也请大家帮忙指出。)
(另外,文章引用的图片or代码如有侵权,请联系我删除。)

微信公众号:AI炼丹术

【手把手教学】手势识别(一) - 项目概述与简单应用介绍 handpose

在这里插入图片描述

一、项目概述

​ 在生活中,我们可以用嘴去和别人说话、沟通;也可以用文本去作为交流的媒介。除此之外,我们也常用手势去传递信息。例如,将食指比在嘴巴处表示闭嘴/不要吵;挥挥手表示打招呼或再见;竖起大拇指表示赞赏;交警会在十字路口通过手势指挥秩序……所以手势是我们生活中人与人交流的一种重要方式。在AI快速发展的时代,我们也想让机器能够和人一样,能够理解人的手势信息,并且做出反应。

​ 所以,今天小编来教大家如何实现手势识别,即让机器看懂你的手势代表什么。

​ 手势识别类似于人体动作识别,常用的基于深度学习的方法有,基于图像序列的LSTM动作识别、基于3D卷积的视频分类以及基于关键点的动作识别。目前主流的方法应该都是基于关键点序列做的动作识别,所以本文我主要也是通过一个手指关键点识别(hand-pose)的项目来开启手势识别的教学。

二、技术简述 & 项目讲解

参考项目:https://codechina.csdn.net/EricLee/dpcas

​ 这个项目是一个大佬写的各种小模块功能集成的系统。包括手部检测、跟踪、手势动作识别、人脸检测、安全帽检测、物体分类等等……有兴趣的朋友可以直接跳转过去仓库作详细了解。对代码有不理解的地方也可以通过issue请教仓库作者。

​ 本文就借这个项目里面手势部分的代码,来讲解手势识别的具体流程和思路。

1. 手部检测

在这里插入图片描述

​ 首先,我们通过一个检测模型去检测到手部。这里作者再dpcas中使用的是yoloV3模型。不过我看作者EricLee的仓库中最近也发布了yolov5的检测模型,但是目前看还未替换进行dpcas中。另外,我们也可以根据自己的场景去使用不同的检测模型。

​ 检测模型训练部分可以到https://codechina.csdn.net/EricLee/yolo_v3下查看,该项目数据集采用 TV-Hand 和 COCO-Hand (COCO-Hand-Big 部分) 进行制作。TV-Hand 和 COCO-Hand数据集官网地址 http://vision.cs.stonybrook.edu/~supreeth/。这些数据集我也看过,更多是偏向自然场景的手部,所以手都会偏小。所以在很多相机前的画面上,检测的精度不是很高(具体表现在视频中可能出现连续的几帧中有检测丢失),因此我们可以尝试去采集适用场景下的数据集对模型进行重新训练或finetune。

​ 模型检测部分作者封装了一个检测模型的类对象,通过 def init 配置好模型的参数,如模型权重路径(model_path)、模型类型(model_arch)、图片大小(img_size)、置信度阈值(conf_thres)、nms阈值(nms_thres)…我们也可以尝试替换成自己的检测模型。

​ 然后通过一个预测函数,def predict(self, img_,vis),实现对模型的推理,返回检测到的目标坐标位置。部分代码如下所示,也可以直接通过下面链接跳转到检测模型的位置。

参考代码:https://codechina.csdn.net/EricLee/dpcas/-/blob/master/components/hand_detect/yolo_v3_hand.py

class yolo_v3_hand_model(object):
    def __init__(self,
        model_path = './components/hand_detect/weights/latest_416-2021-02-19.pt',
        model_arch = 'yolov3',
        yolo_anchor_scale = 1.,
        img_size=416,
        conf_thres=0.16,
        nms_thres=0.4,):
        print("yolo v3 hand_model loading :  {}".format(model_path))
        self.use_cuda = torch.cuda.is_available()
        self.device = torch.device("cuda:0" if self.use_cuda else "cpu")
        self.img_size = img_size
        self.classes = ["Hand"]
        self.num_classes = len(self.classes)
        self.conf_thres = conf_thres
        self.nms_thres = nms_thres
        #-----------------------------------------------------------------------
        weights = model_path
        if "tiny" in model_arch:
            a_scalse = 416./img_size*yolo_anchor_scale
            anchors=[(10, 14), (23, 27), (37, 58), (81, 82), (135, 169), (344, 319)]
            anchors_new = [ (int(anchors[j][0]/a_scalse),int(anchors[j][1]/a_scalse)) for j in range(len(anchors)) ]

            model = Yolov3Tiny(self.num_classes,anchors = anchors_new)
        else:
            a_scalse = 416./img_size
            anchors=[(10,13), (16,30), (33,23), (30,61), (62,45), (59,119), (116,90), (156,198), (373,326)]
            anchors_new = [ (int(anchors[j][0]/a_scalse),int(anchors[j][1]/a_scalse)) for j in range(len(anchors)) ]
            model = Yolov3(self.num_classes,anchors = anchors_new)
        #-----------------------------------------------------------------------

        self.model = model
        # show_model_param(self.model)# 显示模型参数

        # print('num_classes : ',self.num_classes)

        self.device = select_device() # 运行硬件选择
        self.use_cuda = torch.cuda.is_available()
        # Load weights
        if os.access(weights,os.F_OK):# 判断模型文件是否存在
            self.model.load_state_dict(torch.load(weights, map_location=lambda storage, loc: storage)['model'])
        else:
            print('------- >>> error : model not exists')
            return False
        #
        self.model.eval()#模型设置为 eval
        acc_model('',self.model)
        self.model = self.model.to(self.device)

    def predict(self, img_,vis):
        with torch.no_grad():
            t = time.time()
            img = process_data(img_, self.img_size)
            t1 = time.time()
            img = torch.from_numpy(img).unsqueeze(0).to(self.device)

            pred, _ = self.model(img)#图片检测

            t2 = time.time()
            detections = non_max_suppression(pred, self.conf_thres, self.nms_thres)[0] # nms
            t3 = time.time()
            # print("t3 time:", t3)

            if (detections is None) or len(detections) == 0:
                return []
            # Rescale boxes from 416 to true image size
            detections[:, :4] = scale_coords(self.img_size, detections[:, :4], img_.shape).round()
            # 绘制检测结果 :detect reslut
            dets_for_landmarks = []
            colors = [(v // 32 * 64 + 64, (v // 8) % 4 * 64, v % 8 * 32) for v in range(1, 10 + 1)][::-1]

            output_dict_ = []
            for *xyxy, conf, cls_conf, cls in detections:
                label = '%s %.2f' % (self.classes[0], conf)
                x1,y1,x2,y2 = xyxy
                output_dict_.append((float(x1),float(y1),float(x2),float(y2),float(conf.item())))
                if vis:
                    plot_one_box(xyxy, img_, label=label, color=(0,175,255), line_thickness = 2)
            return output_dict_

2. 手部关键点提取

在这里插入图片描述
​ 手部关键点检测模型作者提供了很多不同的backbone,然后输出为21个手指关键点坐标(一个坐标两个数值(x,y)),等于模型输出42维的向量。

​ hand-pose的数据集为:<<Large-scale Multiview 3D Hand Pose Dataset>>数据集,其官网地址 http://www.rovit.ua.es/dataset/mhpdataset/;作者也提供了自己处理后的数据集下载地址:https://pan.baidu.com/s/1KY7lAFXBTfrFHlApxTY8NA(百度网盘 Password: ara8 )。相关细节和模型训练也可以通过仓库https://codechina.csdn.net/EricLee/handpose_x进行复现。

​ 和手部检测一样,模型检测部分作者封装了一个检测模型的类对象,通过 def init 配置好模型的参数,如模型权重路径(model_path)、模型类型(model_arch)、图片大小(img_size)、手部关键点数量(num_classes),手部关键点的数量实际上模型输出的维度,这里指21个关键点,42个数值。

​ 然后通过一个预测函数,def predict(self, img, vis),实现对模型的推理,返回21个关键点的坐标信息。部分代码如下所示,也可以直接通过下面链接跳转到检测模型的位置。

参考代码:https://codechina.csdn.net/EricLee/dpcas/-/blob/master/components/hand_keypoints/handpose_x.py

class handpose_x_model(object):
    def __init__(self,
        model_path = './components/hand_keypoints/weights/ReXNetV1-size-256-wingloss102-0.1063.pth',
        img_size= 256,
        num_classes = 42,# 手部关键点个数 * 2 : 21*2
        model_arch = "rexnetv1",
        ):
        # print("handpose_x loading : ",model_path)
        self.use_cuda = torch.cuda.is_available()
        self.device = torch.device("cuda:0" if self.use_cuda else "cpu") # 可选的设备类型及序号
        self.img_size = img_size
        #-----------------------------------------------------------------------

        if model_arch == 'resnet_50':
            model_ = resnet50(num_classes = num_classes,img_size = self.img_size)
        elif model_arch == 'resnet_18':
            model_ = resnet18(num_classes = num_classes,img_size = self.img_size)
        elif model_arch == 'resnet_34':
            model_ = resnet34(num_classes = num_classes,img_size = self.img_size)
        elif model_arch == 'resnet_101':
            model_ = resnet101(num_classes = num_classes,img_size = self.img_size)
        elif model_arch == "squeezenet1_0":
            model_ = squeezenet1_0(pretrained=True, num_classes=num_classes)
        elif model_arch == "squeezenet1_1":
            model_ = squeezenet1_1(pretrained=True, num_classes=num_classes)
        elif model_arch == "shufflenetv2":
            model_ = ShuffleNetV2(ratio=1., num_classes=num_classes)
        elif model_arch == "shufflenet_v2_x1_5":
            model_ = shufflenet_v2_x1_5(pretrained=False,num_classes=num_classes)
        elif model_arch == "shufflenet_v2_x1_0":
            model_ = shufflenet_v2_x1_0(pretrained=False,num_classes=num_classes)
        elif model_arch == "shufflenet_v2_x2_0":
            model_ = shufflenet_v2_x2_0(pretrained=False,num_classes=num_classes)
        elif model_arch == "shufflenet":
            model_ = ShuffleNet(num_blocks = [2,4,2], num_classes=num_classes, groups=3)
        elif model_arch == "mobilenetv2":
            model_ = MobileNetV2(num_classes=num_classes)
        elif model_arch == "rexnetv1":
            model_ = ReXNetV1(num_classes=num_classes)
        else:
            print(" no support the model")
        #-----------------------------------------------------------------------
        model_ = model_.to(self.device)
        model_.eval() # 设置为前向推断模式

        # 加载测试模型
        if os.access(model_path,os.F_OK):# checkpoint
            chkpt = torch.load(model_path, map_location=self.device)
            model_.load_state_dict(chkpt)
            print('handpose_x model loading : {}'.format(model_path))

        self.model_handpose = model_

    def predict(self, img, vis = False):
        with torch.no_grad():

            if not((img.shape[0] == self.img_size) and (img.shape[1] == self.img_size)):
                img = cv2.resize(img, (self.img_size,self.img_size), interpolation = cv2.INTER_CUBIC)

            img_ = img.astype(np.float32)
            img_ = (img_-128.)/256.

            img_ = img_.transpose(2, 0, 1)
            img_ = torch.from_numpy(img_)
            img_ = img_.unsqueeze_(0)

            if self.use_cuda:
                img_ = img_.cuda()  # (bs, 3, h, w)

            pre_ = self.model_handpose(img_.float())
            output = pre_.cpu().detach().numpy()
            output = np.squeeze(output)

            return output

3. 手部跟踪

​ 这里作者跟踪的代码采用最简单的iou进行跟踪,即使用一个字典记录手的id,通过判断前一帧的手位置跟当前帧的位置的iou来更新手ID的位置信息。这里存在的问题是,如果有多只手重合或手移动过快或目标检测中间帧出现偏差/遗漏,就会导致跟踪出现id-switch的问题。

参考代码:https://codechina.csdn.net/EricLee/dpcas/-/blob/master/lib/hand_lib/cores/tracking_utils.py

def tracking_bbox(data,hand_dict,index,iou_thr = 0.5):

    track_index = index
    reg_dict = {}
    Flag_ = True if hand_dict else False
    if Flag_ == False:
        # print("------------------->>. False")
        for bbox in data:
            x_min,y_min,x_max,y_max,score = bbox
            reg_dict[track_index] = (x_min,y_min,x_max,y_max,score,0.,1,1)
            track_index += 1

            if track_index >= 65535:
                track_index = 0
    else:
        # print("------------------->>. True ")
        for bbox in data:
            xa0,ya0,xa1,ya1,score = bbox
            is_track = False
            for k_ in hand_dict.keys():
                xb0,yb0,xb1,yb1,_,_,cnt_,bbox_stanbel_cnt = hand_dict[k_]

                iou_ = compute_iou_tk((ya0,xa0,ya1,xa1),(yb0,xb0,yb1,xb1))
                # print((ya0,xa0,ya1,xa1),(yb0,xb0,yb1,xb1))
                # print("iou : ",iou_)
                if iou_ > iou_thr: # 跟踪成功目标
                    UI_CNT = 1
                    if iou_ > 0.888:
                        UI_CNT = bbox_stanbel_cnt + 1
                    reg_dict[k_] = (xa0,ya0,xa1,ya1,score,iou_,cnt_ + 1,UI_CNT)
                    is_track = True
                    # print("is_track : " ,cnt_ + 1)
            if is_track == False: # 新目标
                reg_dict[track_index] = (xa0,ya0,xa1,ya1,score,0.,1,1)
                track_index += 1
                if track_index >=65535: #索引越界归零
                    track_index = 0

                if track_index>=100:
                    track_index = 0

    hand_dict = copy.deepcopy(reg_dict)

    # print("a:",hand_dict)

    return hand_dict,track_index

三、代码实战

​ 下面代码是之前作者一开始刚实现手势识别的inference代码,小编去看了下,现在作者更新了整个工具组件后,这部分单独的推理应该是删去了。

​ 简单来说就是,结合了上面第二节所讲到的手部检测、关键点检测、手部跟踪代码后实现的一个整体的功能。由于作者更新过仓库了,所以部分代码的位置也变换了。我这边就不把之前的代码全部罗列出来了,应该都是可以通过函数名找到对应位置,然后替换实现单独的手势关键点检测及跟踪的。

def handpose_x_process(info_dict,config, is_videos, test_path):
    # 模型初始化
    print("load model component  ...")
    # yolo v3 手部检测模型初始化
    hand_detect_model = yolo_v3_hand_model(conf_thres=float(config["detect_conf_thres"]),nms_thres=float(config["detect_nms_thres"]),
        model_arch = config["detect_model_arch"],model_path = config["detect_model_path"])
    # handpose_x 21 关键点回归模型初始化
    handpose_model = handpose_x_model(model_arch = config["handpose_x_model_arch"],model_path = config["handpose_x_model_path"])
    #
    gesture_model = resnet18() # 目前缺省

    gesture_model = gesture_model.cuda()
    gesture_model.load_state_dict(torch.load(config["gesture_model_path"]))

    gesture_model.eval()
    #
    object_recognize_model = None # 识别分类模型,目前缺省

    #
    img_reco_crop = None


    print("start handpose process ~")

    gesture_lines_dict = {} # 点击使能时的轨迹点

    hands_dict = {} # 手的信息
    hands_click_dict = {} #手的按键信息计数
    track_index = 0 # 跟踪的全局索引
    if is_videos:
        cap = cv2.VideoCapture(int(config["camera_id"])) # 开启摄像机
        # cap.set(cv2.CAP_PROP_EXPOSURE, -8) # 设置相机曝光,(注意:不是所有相机有效)

        while True:
            ret, img = cap.read()# 读取相机图像
            if ret:# 读取相机图像成功
                # img = cv2.flip(img,-1)
                algo_img = img.copy()
                st_ = time.time()
                #------
                hand_bbox =hand_detect_model.predict(img,vis = True) # 检测手,获取手的边界框

                hands_dict,track_index = hand_tracking(data = hand_bbox,hands_dict = hands_dict,track_index = track_index) # 手跟踪,目前通过IOU方式进行目标跟踪
                # 检测每个手的关键点及相关信息
                handpose_list = handpose_track_keypoints21_pipeline(img,hands_dict = hands_dict,hands_click_dict = hands_click_dict,track_index = track_index,algo_img = algo_img,
                    handpose_model = handpose_model,gesture_model = gesture_model,
                    icon = None,vis = True)
                et_ = time.time()
                fps_ = 1./(et_-st_+1e-8)
                #------------------------------------------ 跟踪手的 信息维护
                #------------------ 获取跟踪到的手ID
                id_list = []
                for i in range(len(handpose_list)):
                    _,_,_,dict_ = handpose_list[i]
                    id_list.append(dict_["id"])
                # print(id_list)
                #----------------- 获取需要删除的手ID
                id_del_list = []
                for k_ in gesture_lines_dict.keys():
                    if k_ not in id_list:#去除过往已经跟踪失败的目标手的相关轨迹
                        id_del_list.append(k_)
                #----------------- 删除无法跟踪到的手的相关信息
                for k_ in id_del_list:
                    del gesture_lines_dict[k_]
                    del hands_click_dict[k_]

                #----------------- 更新检测到手的轨迹信息,及手点击使能时的上升沿和下降沿信号
                double_en_pts = []
                for i in range(len(handpose_list)):
                    _,_,_,dict_ = handpose_list[i]
                    id_ = dict_["id"]
                    if dict_["click"]:
                        if  id_ not in gesture_lines_dict.keys():
                            gesture_lines_dict[id_] = {}
                            gesture_lines_dict[id_]["pts"]=[]
                            gesture_lines_dict[id_]["line_color"] = (random.randint(100,255),random.randint(100,255),random.randint(100,255))
                            gesture_lines_dict[id_]["click"] = None
                        #判断是否上升沿
                        if gesture_lines_dict[id_]["click"] is not None:
                            if gesture_lines_dict[id_]["click"] == False:#上升沿计数器
                                info_dict["click_up_cnt"] += 1
                        #获得点击状态
                        gesture_lines_dict[id_]["click"] = True
                        #---获得坐标
                        gesture_lines_dict[id_]["pts"].append(dict_["choose_pt"])
                        double_en_pts.append(dict_["choose_pt"])
                    else:
                        if  id_ not in gesture_lines_dict.keys():
                            gesture_lines_dict[id_] = {}
                            gesture_lines_dict[id_]["pts"]=[]
                            gesture_lines_dict[id_]["line_color"] = (random.randint(100,255),random.randint(100,255),random.randint(100,255))
                            gesture_lines_dict[id_]["click"] = None
                        elif  id_ in gesture_lines_dict.keys():

                            gesture_lines_dict[id_]["pts"]=[]# 清除轨迹
                            #判断是否上升沿
                            if gesture_lines_dict[id_]["click"] == True:#下降沿计数器
                                info_dict["click_dw_cnt"] += 1
                            # 更新点击状态
                            gesture_lines_dict[id_]["click"] = False

                #绘制手click 状态时的大拇指和食指中心坐标点轨迹
                draw_click_lines(img,gesture_lines_dict,vis = bool(config["vis_gesture_lines"]))
                # 判断各手的click状态是否稳定,且满足设定阈值
                flag_click_stable = judge_click_stabel(img,handpose_list,int(config["charge_cycle_step"]))

                cv2.putText(img, 'HandNum:[{}]'.format(len(hand_bbox)), (5,25),cv2.FONT_HERSHEY_COMPLEX, 0.7, (255, 0, 0),5)
                cv2.putText(img, 'HandNum:[{}]'.format(len(hand_bbox)), (5,25),cv2.FONT_HERSHEY_COMPLEX, 0.7, (0, 0, 255))

                cv2.namedWindow("image",0)
                cv2.imshow("image",img)
                # if cv2.waitKey(1) == 27:
                #     info_dict["break"] = True
                #     break
            else:
                break

        cap.release()
        cv2.destroyAllWindows()
    else:
        for img_name in os.listdir(test_path):
            print(img_name)
            img = cv2.imread(os.path.join(test_path,img_name))
            algo_img = img.copy()
            hand_bbox = hand_detect_model.predict(img, vis=True)  # 检测手,获取手的边界框

            hands_dict, track_index = hand_tracking(data=hand_bbox, hands_dict=hands_dict,
                                                    track_index=track_index)  # 手跟踪,目前通过IOU方式进行目标跟踪
            # 检测每个手的关键点及相关信息
            handpose_list = handpose_track_keypoints21_pipeline(img, hands_dict=hands_dict,
                                                                hands_click_dict=hands_click_dict,
                                                                track_index=track_index, algo_img=algo_img,
                                                                handpose_model=handpose_model,
                                                                gesture_model=gesture_model,
                                                                icon=None, vis=True)
            cv2.namedWindow("image", 0)
            cv2.imshow("image", img)
            cv2.waitKey(0)

            et_ = time.time()

四、效果展示及总结

​ 通过上面的代码讲解,我们可以简单实现手部的检测、关键点检测、跟踪……这些简单的功能了。

​ 然后具体到手势识别部分,作者是提供了9种静态动作(fist、five、gun、love、one、six、three、thumbup、yeah),我看了一下好像没有加在整体的代码里面,所以这部分功能应该是缺失的,只有一个点击检测(click)。

​ 作者仓库提供的静态动作识别是通过手指关键点的角度信息制定相应的规则进行动作识别的。我这边根据作者的建议(注:这种静态手势识别的方法具有局限性,有条件还是通过模型训练的方法进行静态手势识别。),利用作者提供的数据集(静态手势数据集的限制,数据集过少),自己利用一个简单的分类网络进行训练,成功实现了9种动作的分类,并加在了整体的代码里面。由于篇幅会拉得比较长,本文就想讲解到这里,有兴趣的小伙伴可以先自己实现前面这些简单的功能。

​ 具体我是如何通过少量数据集实现一个鲁棒性比较强的静态动作分类网络、如何提高跟踪的准确性(降低id-switch)、如何实现动态动作的识别,我将在后续的文章逐步进行讲解。
在这里插入图片描述
在这里插入图片描述

五、参考

EricLee:https://codechina.csdn.net/EricLee (本文主要借鉴这位大佬的开源代码。)

  • 4
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值