【关键点检测】yolov7-pose改造——任意数量关键点检测并训练自己的数据集

一、背景

工作项目需要用到关键点检测,yolo系列在工业视觉领域应用相对广泛,技术积累相对成熟(翻译:出bug方便搜),在检测速度和准确率方面也表现优异,毫无疑问成为首选。作为彼时yolo家族的最新款,本人也想尝尝鲜。

原始的yolov7-pose是针对人体17个关键点的检测模型,因此需要对其改造以适应项目需求(项目的使用场景中只需要3个关键点),开始漫长的改造和调试之路。

源码:GitHub - WongKinYiu/yolov7 at pose

 二、具体工作

1. 准备数据集

数据标注工具使用的是labelme,生成标注文件,由于labelme无法直接生成yolo格式的标签,因此需要将其生成的coco格式的json文件转换为yolo格式的txt文件。

yolo格式的关键点标签文件内容如下:

前五个(cls, c_x, c_y, w, h)为目标类别和标注框中心点和长宽,根据图像宽高统一进行归一化,后面即为关键点坐标和关键点可见度(kpx_1, kpy_1, v_1, kpx_2, kpy_2, v_2, ..., kpx_i, kpy_i, v_i)xy同样进行了归一化操作,v=0, 1, 2,0表示该点不存在,1表示该点不可见(遮挡),2表示该点可见。

转换代码网上很多,不多做介绍,主要参考[1]

数据集生成之后,在data/coco_kpts.yaml中修改自己的数据集路径及类别信息。

2. 代码修改

数据集准备好之后就可以进行模型训练了,如果需要预训练权重可以从作者github上下载。由于官方提供的是针对人体关键点的检测代码,且并没有提供一个统一的关键点数量修改入口,在数据集加载、损失函数计算、画图等文件里默认都是1个类别和17个关键点,所以训练之前需要自己修改相关代码文件,将其中与关键点数量相关的部分全都修改为自己需要的关键点数量。

(1)cfg/yolov7-w6-pose.yaml

模型结构和anchor的配置文件,将里面的nc和nkpt修改为自己需要检测的类别数和关键点数量。

(2)model/yolo.py

Detect()对应的是yolo的head部分,处理模型最终的预测值,将其转换为我们最终需要的预测坐标和类别信息等。此脚本中作者在yolov5的Detect()中加入了对关键点预测信息的处理方法,并新增了IDetect()IKeypoint(),三者之间大同小异,用哪个都可以,以下用IKeypoint()为例进行修改。

 Line265-266,坐标种类划分

# 将6改为5+nc
x_det = x[i][..., :5 + self.nc]
x_kpt = x[i][..., 5 + self.nc:]

Line283-284,关键点坐标处理

# 17改为nkpt
x_kpt[..., 0::3] = (x_kpt[..., ::3] * 2. - 0.5 + kpt_grid_x.repeat(1,1,1,1,self.nkpt)) * self.stride[i]  # xy
x_kpt[..., 1::3] = (x_kpt[..., 1::3] * 2. - 0.5 + kpt_grid_y.repeat(1,1,1,1,self.nkpt)) * self.stride[i]  # xy

此部分修改仅在模型推理的后处理阶段有效,训练过程中并不会用到。

(3)utils/datasets.py

create_dataloader()LoadImagesAndLabels()random_perspective()中添加参数nkpt

def create_dataloader(path, imgsz, batch_size, stride, opt, hyp=None, augment=False, cache=False, pad=0.0, rect=False,
                      rank=-1, world_size=1, workers=8, image_weights=False, quad=False, prefix='', tidl_load=False,
                      kpt_label=False, nkpt=0):
class LoadImagesAndLabels(Dataset):  # for training/testing
    def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, rect=False, image_weights=False,
                 cache_images=False, single_cls=False, stride=32, pad=0.0, prefix='', square=False, tidl_load=False,
                 kpt_label=True, nkpt=0):
def random_perspective(img, targets=(), segments=(), degrees=10, translate=.1, scale=.1, shear=10, perspective=0.0,
                       border=(0, 0), kpt_label=False, nkpt=0):

处理标签数据,在LoadImagesAndLabels()中添加成员变量nkpt,同时修改操作关键点翻转的flip_index

self.nkpt = nkpt
"""
源文件中flip_index = [0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15],分别表示:
[0鼻子,1左眼,2右眼,3左耳,4右耳,5左键,6右肩,7左肘,8右肘,9左腕,10右腕,11左胯,12右胯,13左膝,14右膝,15左踝,16右踝]
除0之外,其它成对关键点两两交换,实现左右翻转效果,可以根据自己的关键点要求进行修改,比如标注三个关键点[0, 1, 2],前两个关键点可以进行翻转,则flip_index=[1, 0, 2]
"""
self.flip_index = [1, 0, 2]

LoadImagesAndLabels()cache_labels()方法

Line 496

# 56改为5+nkpt*3
assert l.shape[1] == 5 + self.nkpt * 3, 'labels require 14 columns each'

Line 500,505,513,517,39改为5+nkpt*3

Line 592后添加

img, labels = random_perspective(img, labels,
                                 degrees=hyp['degrees'],
                                 translate=hyp['translate'],
                                 scale=hyp['scale'],
                                 shear=hyp['shear'],
                                 perspective=hyp['perspective'],
                                 kpt_label=self.kpt_label,
                                 nkpt=self.nkpt)     # nkpt传参

Line 986-987random_perspective(),17改为nkpt

Line 989-997,34改为2*nkpt

(4)utils/loss.py

 ComputeLoss添加参数nkpt和nc

class ComputeLoss:
    # Compute losses
    def __init__(self, model, autobalance=False, kpt_label=False, nkpt=17, nc=1):
        super(ComputeLoss, self).__init__()
        self.kpt_label = kpt_label
        self.nkpt = nkpt
        self.nc = nc

Line 119,修改sigmas,长度=nkpt,修改原则参考[2],简单点说sigmas中每个元素值(OKS中的k_n)对应一个关键点的惩罚,值越小惩罚力度越大,相应模型对这些关键点的学习关注度越高,具体值的设定沿用yolo-pose[5],论文中说是从coco验证集中得出的经验值。

原始代码中sigmas=[.26, .25, .25, .35, .35, .79, .79, .72, .72, .62, .62, 1.07, 1.07, .87, .87, .89, .89]/10.0,所以修改也尽量参考这些值。

def __call__(self, p, targets):  # predictions, targets, model
        device = targets.device
        lcls, lbox, lobj, lkpt, lkptv = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device)
        # 修改sigmas
        sigmas = torch.tensor([.79, .79, .25], device=device) / 10.0

Line 187,build_targets(),41改为7+2*nkpt

Line 202,40改为6+2*nkpt,19改为2+nkpt

(5)utils/general.py

Line 493,590,56改为3*nkpt+5

non_max_suppression()

non_max_suppression_export()

Line 594,57改为6+3*nkpt

Line 506,540-541,6改为5+nc[3]

(6)utils/common.py 

网络组件,根据[2]中作者的解释,如果使用的模型是基于较新版本的yolov5, 模型组件有所变化,SPP替换为SPPF,因此需要在common.py中添加SPPF的实现。但yolov7-pose中作者提供的yolov7-w6-pose.yaml文件中没有使用SPPF模块,所以加不加都可以。

class SPPF(nn.Module):
    # Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher
    def __init__(self, c1, c2, k=5):  # equivalent to SPP(k=(5, 9, 13))
        super().__init__()
        c_ = c1 // 2  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_ * 4, c2, 1, 1)
        self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2)

    def forward(self, x):
        x = self.cv1(x)
        with warnings.catch_warnings():
            warnings.simplefilter('ignore')  # suppress torch 1.9.0 max_pool2d() warning
            y1 = self.m(x)
            y2 = self.m(y1)
            return self.cv2(torch.cat([x, y1, y2, self.m(y2)], 1))
(7)utils/plot.py

Line  97skeleton,绘制骨架结构,哪两个关键点之间需要连接就添加一组,比如我检测的三个关键点,1和2需要连接,3不需要连接,可以这样写:

skeleton = [[1, 2], [3, 3]]

Line 101,pose_limb_color,绘制连线颜色(猜的);

Line 102,pose_kpt_color,绘制关键点颜色(猜的)

这两处根据自己的关键点数量修改,从palette调色板中选颜色,不改也可以。

Line 219,40改为2*nkpt+6,参考[2]热评

 

(8)train.py

Line 201,211,create_dataloader()传参nkpt

dataloader, dataset = create_dataloader(train_path, imgsz, batch_size, gs, opt,
                                        hyp=hyp, augment=True, cache=opt.cache_images, rect=opt.rect, rank=rank,
                                        world_size=opt.world_size, workers=opt.workers,
                                        image_weights=opt.image_weights,quad=opt.quad, prefix=colorstr('train: '), kpt_label=kpt_label,
                                        nkpt=nkpt)

Line 369,441,test()传参nkpt

results, _, _ = test.test(opt.data,
                          batch_size=batch_size * 2,
                          imgsz=imgsz_test,
                          conf_thres=0.001,
                          iou_thres=0.7,
                          model=attempt_load(m, device).half(),
                          single_cls=opt.single_cls,
                          dataloader=testloader,
                          save_dir=save_dir,
                          save_json=True,
                          plots=False,
                          is_coco=is_coco,
                          kpt_label=kpt_label,
                          nkpt=nkpt)
(9)test.py

Line 22,test()添加参数nkpt

def test(data,
         weights=None,
         batch_size=32,
         imgsz=640,
         conf_thres=0.001,
         iou_thres=0.6,  # for NMS
         save_json=False,
         save_json_kpt=False,
         single_cls=False,
         augment=False,
         verbose=False,
         model=None,
         dataloader=None,
         save_dir=Path(''),  # for saving images
         save_txt=False,  # for auto-labelling
         save_txt_tidl=False,  # for auto-labelling
         save_hybrid=False,  # for hybrid auto-labelling
         save_conf=False,  # save auto-label confidences
         plots=True,
         wandb_logger=None,		 
		 compute_loss=None,
         half_precision=True,
         is_coco=False,
         opt=None,		 		 
         tidl_load=False,
         dump_img=False,
         kpt_label=False,
         flip_test=False,
         nkpt=17):

Line 80,flip_index,与datasets.pyLoadImagesAndLabels()flip_index相同

Line 99,create_dataloader()传参nkpt

dataloader = create_dataloader(data[task], imgsz, batch_size, gs, opt, pad=0.5, rect=True,
                               prefix=colorstr(f'{task}: '), tidl_load=tidl_load, kpt_label=kpt_label, nkpt=nkpt)[0]

至此,完成所有需要修改的位置,可以训练自己的数据集了。

三、问题

本人在初次使用过程中遇到过各种bug,除去一些低级错误和忘掉的错误,总结一下印象比较深刻的一些问题。

1. 某个关键点预测偏差大

训练完成后,发现模型的预测结果中三个关键点里总有一个点预测飞了(时隔太久图已经删了),效果类似于这样

试过是否模型没有收敛?标签生成有问题?那个参数设置的不对?模型后处理有问题?修改关键点数量的时候那个地方没改对?最终在这里找到解决方法(大佬牛逼!)

https://github.com/TexasInstruments/edgeai-yolov5/issues/47

修改loss.py中OKS的指数部分的计算方法,论文中OKS的指数的分母是2s^2k_n^2,不知道为啥代码里就变成了4sk_n^2

还有一种修改方式,s改为s^{1.1},但这种方式没有试过,不知道是否有效

2. 模型导出

如果需要将模型部署到边缘设备, 比如部署到CPU/NPU/TPU上进行加速,都要把模型转换到特定格式,转换之前要先把权重文件导出为onnx模型,同时修改yolo.pyDetect()部分。

def forward(self, x):
    # x = x.copy()  # for profiling
    z = []  # inference output
    self.training |= self.export
    for i in range(self.nl):
        if self.nkpt is None or self.nkpt==0:
            x[i] = self.m[i](x[i])
        else :
            x[i] = torch.cat((self.m[i](x[i]), self.m_kpt[i](x[i])), axis=1)

        bs, _, ny, nx = x[i].shape  # x(bs,255,20,20) to x(bs,3,20,20,85)
        # 注释掉此行
        # x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
        x_det = x[i][..., :6]
        x_kpt = x[i][..., 6:]

        if not self.training:  # inference
            if self.grid[i].shape[2:4] != x[i].shape[2:4]:
                self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
            kpt_grid_x = self.grid[i][..., 0:1]
            kpt_grid_y = self.grid[i][..., 1:2]

            if self.nkpt == 0:
                y = x[i].sigmoid()
            else:
                y = x_det.sigmoid()

            if self.inplace:
                # 此处的y改为x_det
                xy = (x_det[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i]  # xy
                wh = (x_det[..., 2:4] * 2) ** 2 * self.anchor_grid[i].view(1, self.na, 1, 1, 2) # wh
                if self.nkpt != 0:
                    # 此处的17改为关键点数量
                    x_kpt[..., 0::3] = (x_kpt[..., ::3] * 2. - 0.5 + kpt_grid_x.repeat(1,1,1,1,self.nkpt)) * self.stride[i]  # xy
                    x_kpt[..., 1::3] = (x_kpt[..., 1::3] * 2. - 0.5 + kpt_grid_y.repeat(1,1,1,1,self.nkpt)) * self.stride[i]  # xy
                    #x_kpt[..., 0::3] = ((x_kpt[..., 0::3].tanh() * 2.) ** 3 * self.anchor_grid[i][:,0].repeat(self.nkpt,1).permute(1,0).view(1, self.na, 1, 1, self.nkpt)) + kpt_grid_x.repeat(1,1,1,1,17) * self.stride[i]  # xy
                    #x_kpt[..., 1::3] = ((x_kpt[..., 1::3].tanh() * 2.) ** 3 * self.anchor_grid[i][:,0].repeat(self.nkpt,1).permute(1,0).view(1, self.na, 1, 1, self.nkpt)) + kpt_grid_y.repeat(1,1,1,1,17) * self.stride[i]  # xy
                    x_kpt[..., 2::3] = x_kpt[..., 2::3].sigmoid()
                # 此处的y改为x_det
                y = torch.cat((xy, wh, x_det[..., 4:], x_kpt), dim = -1)

            else:  # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953
                xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i]  # xy
                wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
                if self.nkpt != 0:
                    y[..., 6:] = (y[..., 6:] * 2. - 0.5 + self.grid[i].repeat((1,1,1,1,self.nkpt))) * self.stride[i]  # xy
                y = torch.cat((xy, wh, y[..., 4:]), -1)

            z.append(y.view(bs, -1, self.no))

    return x if self.training else (torch.cat(z, 1), x)

最终输出形状,参考[4]

四、总结

第一次使用关键点检测模型并进行魔改,说实话前期还是费了不少功夫的,在没有人带的情况下只能自己慢慢摸索,最终总算是走通了,也用到了项目里,过程当中也学到了不少东西,总感觉不记下来时间长了就忘记了,比如调过的bug很多已经忘了,绝不止前面两个。所以时隔一年多,又从头捋了一遍,把这个过程尽量详细的记录下来,就当复习了。

yolov7-pose在yolo-pose的基础上改了模型结构,层数更深,多了一个特征图尺度。经过测试,在预训练权重上进行训练比从零训练准确率要高一些,yolov7-pose比yolo-pose的准确率要高,看来增加模型深度和特征图尺度确实有利于模型学习。如果想进一步提升准确率,之前尝试过加入CBAM模块,准确率有微小提升(大概1%),模型参数会增加,训练会变慢,这个根据实际需要来吧。 

本人技术菜鸡,写文章纯属学习记录,大佬勿喷。 

参考资料

[1] Yolov7-pose 训练body+foot关键点-CSDN博客

[2] https://zhuanlan.zhihu.com/p/603799078 

[3] 基于yoloV7-pose添加任意关键点 + 多类别分类网络修改_yolov7s-pose分类代码-CSDN博客 

[4] GitHub - nanmi/yolov7-pose: pose detection base on yolov7

[5] https://arxiv.org/abs/2204.06806

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
训练yolov7-pose模型,您需要按照以下步骤进行操作: 1. 首先,您需要制作适用于yolov7-pose数据集。可以参考引用中提供的链接中关于制作YOLO格式数据集的说明。您需要标注每个图像中的人体姿势关键点,并生成相应的标签文件。 2. 下载yolov7-pose的代码和预训练模型。您可以使用引用中提供的git命令来下载代码。命令如下: `git clone https://github.com/wongkinyiu/yolov7` 3. 准备好训练所需的文件和目录结构。将您的训练图像放置在一个文件夹中,将其标签文件放置在另一个文件夹中。确保标签文件的命名与相应图像文件的命名一致。 4. 运行训练脚本开始训练。您可以使用引用提供的训练信息保存路径来保存训练信息。具体的训练命令如下: `python train.py --data coco.yaml --weights yolov7.pt --cfg models/yolov5s.yaml --batch-size 16` 这里,`--data`参数指定了数据集的配置文件,`--weights`参数指定了预训练模型的路径,`--cfg`参数指定了模型的配置文件,`--batch-size`参数指定了每个批次的图像数量。 5. 等待训练完成。训练过程可能会花费一些时间,具体时间取决于您的数据集大小和训练配置。 6. 训练完成后,您可以在yolov7/runs/train/exp目录下找到保存的所有训练信息,包括训练权重和日志文件。 希望这些步骤对您有帮助!如果您还有其他问题,请随时提问。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值