[开源]基于姿态估计的运动计数APP开发(二)

1、先展示一下当前的效果

从keep上扒了一段仰卧起坐的视频教程进行计数测试:

(CSDN放不了视频,有兴趣的下方评论区留言)

2、回顾:

在上一期的内容中([开源]基于姿态估计的运动计数APP开发(一)),通过使用shufflenet轻量级网络+上采样输出关键点的heatmap已经可以在coco数据集中进行训练,并能够进行关键点识别。但是也存在一个问题,就是针对仰卧起坐这种动作,识别准确率非常低。通过分析原因,主要有两方面。一是开源的数据集中人的姿态是一些比较生活化的姿态,很少有仰卧起坐之类的姿态。另外一方面是网络本身比较小,提取特征能力有限,且为了在移动端实现实时检测,输出分辨率被限制在224*224,这些都会限制精度。本期主要是基于这些问题展开一些优化,初步实现一个可以进行实时计数的demo。

3、重新思考数据:

前面已经分析了coco以及mpii这些数据集对于我们训练仰卧起坐计数器这样一个APP来说并不是非常合适。并且对于这个任务来说,其实并不需要识别那么多的关键点。只要识别两个关键点就可以了,一个是头部关键点,一个是膝盖关键点。这样的话既不会影响我们最终计数APP的功能,又使得网络的任务减轻,可以专注与识别这两个关键点,从而提升精度。如下图所示。

[开源]基于姿态估计的运动计数APP开发(二)_第1张图片

由于没有现成的仰卧起坐数据集,只能自己动手,丰衣足食。好在对于仰卧起坐这样常规的运动,网上还是有很多相关资源的。这里我采用下载视频和图片两种方式。先从网上搜索“仰卧起坐”的视频,下载了10个左右的视频片段,然后通过抽帧的方式,从每个视频中抽取一部分帧作为训练用的数据。如下图所示为从视频中抽取的关键帧。

[开源]基于姿态估计的运动计数APP开发(二)_第2张图片

仅仅使用视频中抽取的帧会有一个比较严重的问题,就是背景过于单一,很容易造成过拟合。于是我从网上进行图片搜索,得到一分部背景较为丰富的图片,如下图所示:

[开源]基于姿态估计的运动计数APP开发(二)_第3张图片

收集完数据,就是进行标注了,这里我为了方便,自己开发了一款关键点标注工具,毕竟自己开发的,用着顺手。鼠标左键进行标注,右键取消上一次标注。不得不说,用python+qt开发一些基于UI的工具非常方便!与C++相比,解放了太多的生产力!

[开源]基于姿态估计的运动计数APP开发(二)_第4张图片

4、解决过拟合:

    通过前面搜集的大约1K张图片,训练完之后会发现泛化性能并不好,很容易出现误识别。其实不难发现,由于数据量太少,网络已经出现了过拟合,训练到最后的loss非常小。解决过拟合最好的办法是增加数据量,但是时间有限,真的不想再去收集,标注数据,简直是浪费青春啊。于是就得考虑用一些数据增强的方法。我之前已经使用了一些光照增强的方法,例如随机改变亮度,随机调整HSV,这里主要增加一些几何上的变换。由于需要修改标签值,因此会麻烦一些。这里我主要考虑crop,padding,以及flip。

[开源]基于姿态估计的运动计数APP开发(二)_第5张图片

用来上述数据增强方法之后,效果显著改善了一些,过拟合没有那么严重,但是会出现一些错误的召回,会把一些书包或者衣服之类的当作关键点。那么主要原始还是训练数据的背景不够丰富,这里采用mixup的方法,从coco数据集中挑选一部分没有人的图片作为背景,随机的与训练图片进行重叠,从而有利于解决这种问题。

[开源]基于姿态估计的运动计数APP开发(二)_第6张图片

最终经过这些数据增强之后效果还不错。下面是相关代码,crop和padding合在一起实现。

class KPRandomPadCrop(object):
    def __init__(self, ratio=0.25, pad_value=[128, 128, 128]):
        assert (ratio > 0 and ratio <= 1)
        self.ratio = ratio
        self.pad_value = pad_value

    def __call__(self, image, labels=None):
        if random.randint(0,1):
            h, w = image.shape[:2]
            top_offset = int(h * random.uniform(0, self.ratio))
            bottom_offset = int(h * random.uniform(0, self.ratio))
            left_offset = int(w * random.uniform(0, self.ratio))
            right_offset = int(w * random.uniform(0, self.ratio))
            # pad
            if random.randint(0,1):
                image = cv2.copyMakeBorder(image, top_offset, bottom_offset, left_offset, right_offset, cv2.BORDER_CONSTANT, value=self.pad_value)
                if labels is not None and len(labels) > 0:
                    labels[:, 0] = (labels[:, 0] * w + left_offset) / (w + left_offset + right_offset)
                    labels[:, 1] = (labels[:, 1] * h + top_offset) / (h + top_offset + bottom_offset)
            # crop
            else:
                image = image[top_offset:h - bottom_offset, left_offset:w-right_offset]
                if labels is not None and len(labels) > 0:
                    labels[:, 0] = (labels[:, 0] * w - left_offset) / (w - left_offset - right_offset)
                    labels[:, 1] = (labels[:, 1] * h - top_offset) / (h - top_offset - bottom_offset)
        return image, labels
                
class KPRandomHorizontalFlip(object):
    def __init__(self):
        pass

    def __call__(self, image, labels=None):
        if random.randint(0, 1):
            image = cv2.flip(image, 1)
            h, w = image.shape[:2]
            if labels is not None and len(labels) > 0:
                labels[:, 0] = 1.0 - labels[:, 0]
        return image, labels
        
  
class KPRandomNegMixUp(object):
    def __init__(self, ratio=0.5, neg_dir='./coco_neg'):
        self.ratio = ratio
        self.neg_dir = neg_dir
        self.neg_images = []
        files = os.listdir(self.neg_dir)
        for file in files:
            if str(file).endswith('.jpg') or str(file).endswith('.png'):
                self.neg_images.append(str(file))

    def __call__(self, image, labels):
        if random.randint(0, 1):
            h, w = image.shape[:2]
            neg_name = random.choice(self.neg_images)
            neg_path = self.neg_dir + '/' + neg_name
            neg_img = cv2.imread(neg_path)
            neg_img = cv2.resize(neg_img, (w, h)).astype(np.float32)
            neg_alpha = random.uniform(0, self.ratio)
            ori_alpha = 1 - neg_alpha
            gamma = 0
            img_add = cv2.addWeighted(image, ori_alpha, neg_img, neg_alpha, gamma)
            return image, labels
        else:
            return image, labels

5、在线难例挖掘

    通过上面的数据增强,训练出来的模型已经具备了一定的能力,但是通过大量的测试图片发现网络对于膝盖和头部的检测能力是不一样的。膝盖的检测较为稳定,而头部的检测经常会出现错误。分析原因,可能在于头部的变化比膝盖大的多,头部可能是正对相机,背对相机,也有可能被手臂挡住,因为网络很难学习到真正的头部特征。这里可以通过两种方法来改善,一种是简单的通过loss权重来赋予头部更大的权值,使得头部的梯度信息比膝盖的大,强迫网络更关注头部信息,还有一种就是用到了online hard keypoint mining。这是我在查看旷世的cpn人体姿态估计网络时候看到的,作者有视频进行介绍。其实实现起来很简单,就是对不同的关键点分别求loss,然后对loss排序,只返回一定比例的loss求梯度,作者给出的比例是0.5,即返回一半的关键点loss来求梯度。

[开源]基于姿态估计的运动计数APP开发(二)_第7张图片

6、总结

    这个阶段主要是对网络精度的提升,通过精简关键点数目,重新收集,标注数据,增加padding,crop,flip数据增强,并引入mixup和在线难例挖掘等措施,来逐步提升网络泛化性能。并实现了一个python的demo(见文章开头),下一期主要是将该demo的功能实现为APP。

@end

你可能感兴趣的:(深度学习)