目标检测、人体姿态估计与动作捕捉和目标追踪在YOLO系列算法中的实现与应用

YOLO及You Only Look Once,是一种目标检测算法,目标检测任务的目标是找到图像中的所有感兴趣区域,并确定这些区域的位置和类别概率。目标检测领域的深度学习方法主要分为两大类(如图1):两阶段式(Two-stage)目标检测算法和单阶段式(One-stage)目标检测算法。两阶段式是先由算法生成一系列候选边界框作为样本,然后再通过卷积神经网络分类这些样本,也被称为基于区域的方法,例如R-CNN、Fast R-CNN、Faster R-CNN、R-FCN等;后者则是直接将目标边界定位问题转换成回归问题,图像会被缩放到同一尺寸,并以网格形式均等划分,模型仅需处理图像一次就能得到边界框坐标跟类概率,例如MultiBox、YOLO、SSD等。

1. 目标检测

1.1 项目的克隆部署和必要的环境依赖
1.1.1 YOLO简介及部署

两阶段式(Two-stage)和单阶段式(One-stage)两种目标检测算法的区别也导致其性能也不同,前者在检测准确率和定位准确率方面更优,而后者胜在算法速度。YOLO是一个one-stage的目标检测算法,我们可以将其看作单一的回归问题。

本次目标检测采用YOLO v5版本,具体的版本号为YOLO v5.0,点击仓库页面左上角的Tags选项,从更新的v1.0至v6.2版本号中选择v5.0,下载源码文件即可。

1.1.2 YOLO项目明细说明

​ 我们使用PyCharm IDE打开项目文件,下面对代码的一些目录和文件进行简要说明。

  • data文件夹:主要是存放一些超参数的配置文件(这些文件(yaml文件)是用来配置训练集和测试集还有验证集的路径的,其中还包括目标检测的种类数和种类的名称);还有一些官方提供测试的图片。如果是训练自己的数据集的话,那么就需要修改其中的yaml文件。但是自己的数据集不建议放在这个路径下面,而是建议把数据集放到yolov5项目的同级目录下面。
  • models文件夹:里面主要是一些网络构建的配置文件和函数,其中包含了该项目的四个不同的版本,分别为是s、m、l、x。顾名思义,即为small,medium,large和最大。他们的检测测度分别都是从快到慢,但是精确度分别是从低到高。如果训练自己的数据集的话,就需要修改这里面相对应的yaml文件来训练自己模型。
  • utils文件夹:存放的是工具类的函数,里面有loss函数,metrics函数,plots函数等等。
  • weights文件夹:放置训练好的权重参数。
  • detect.py:利用训练好的权重参数进行目标检测,可以进行图像、视频和摄像头的检测。
  • train.py:训练自己的数据集的函数。
  • test.py:测试训练的结果的函数。
  • requirements.txt:这是一个文本文件,里面写着使用yolov5项目的环境依赖包的一些版本,可以利用该文本导入相应版本的包。
1.1.3 环境依赖的安装

其中关于深度学习的环境而言,需要安装NVIDIA独立显卡的GeForce Experience驱动程序,可以通过nvidia-smi找到驱动版本号之后,得知最小满足的CUDA版本要求。然后下载Anaconda,配置好系统环境变量,利用conda env list查看当前拥有的虚拟环境,输入conda create -n pytorch python=3.8给PyTorch创建虚拟环境,而后使用conda activate pytorch激活PyTorch虚拟环境。然后前往PyTorch官网,使用Conda运行命令行的方式来安装,例如运行conda install pytorch torchvision torchaudio pytorch-cuda=11.7 -c pytorch -c nvidia来进行安装。

至于验证CUDA和CUDNN,我们可以在PyCharm里面输入下列命令观察输出结果。

1
2
3
4
5
import torch
print(torch.cuda.is_available())
print(torch.backends.cudnn.is_available())
print(torch.cuda_version)
print(torch.backends.cudnn.version())

而后进行项目所需环境依赖的安装。进入安装目录,运行下列指令:pip install -r requirements.txt,然后完成深度学习环境的依赖安装。

1.2 训练集和预训练权重的准备
1.2.1 利用labelimg工具进行标注

Labelimg是一款开源的数据标注工具,可以标注三种格式。分别为VOC标签格式,保存为xml文件; yolo标签格式,保存为txt文件;createML标签格式,保存为json格式。

至于安装labelimg软件,进入cmd命令行控制台。输入如下的命令:

1
pip install labelimg -i https://pypi.tuna.tsinghua.edu.cn/simple

首先新建一个名为VOC2022的文件夹,里面创建一个名为JPEGImages的文件夹存放我们需要打标签的图片文件;再创建一个名为Annotations存放标注的标签文件;最后创建一个名为 predefined_classes.txt 的txt文件来存放所要标注的类别名称。然后在 predefined_classes.txt 这个txt文档里面输入定义的类别种类,例如下列所示,每个类别另起一行。

1
2
3
person
dog
cat

CMD进入到VOC2022的文件夹目录下, 输入如下的命令打开labelimg。这个命令的意思是打开labelimg工具;打开JPEGImage文件夹,初始化predefined_classes.txt里面定义的类。

1
labelimg JPEGImages predefined_classes.txt

常用快捷键如下:A:切换到上一张图片;D:切换到下一张图片;W:调出标注十字架;del :删除标注框框;Ctrl+u:选择标注的图片文件夹;Ctrl+r:选择标注好的label标签存在的文件夹。

由于我们设置标注的十字架一直在标注界面上,这就不需要我们按快捷键w,然后选定我们需要标注的对象。按住鼠标左键拖出框框就可以了。如下图所示,当我们选定目标以后,就会加载出来predefined_classes.txt 定义自己要标注的所有类别(如果类别多,是真的很方便,就不需要自己手打每个类别的名字了)。打好的标签框框上会有该框框的类别(图中由于颜色的原因不太清晰,仔细看会发现的)。然后界面最右边会出现打好的类别标签。打好一张照片以后,快捷键D,就会进入下一张,这时候就会自动保存标签文件(voc格式会保存xml,yolo会保存txt格式)。

标签打完以后可以去Annotations 文件下看到标签文件已经保存在Annotation这个目录下。

1.2.2 标签格式的转换
  • VOC数据格式: VOC格式文件保存在和图像名称一样的xml文件中,可以更直观的查看。

  • YOLO数据格式: YOLO数据格式是把每个图片的标注信息保存在一个和图片名称一样的txt文件中。txt文件中的信息如下图所示:

1
2
0 0.47416020671834624 0.4523809523809524 0.5968992248062015 0.683982683982684
1 0.874031007751938 0.4069264069264069 0.1227390180878553 0.2727272727272727

每一行代表标注的一个目标,第一个数字代表着这个数的类别,第一类目标就是0,第二类目标就是1,以此类推。后面的四个数字是归一化后的的标注的中心点坐标和归一化标注框的长和宽。

它们之间的互相装换之voc转yolo代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join


def convert(size, box):
x_center = (box[0] + box[1]) / 2.0
y_center = (box[2] + box[3]) / 2.0
x = x_center / size[0]
y = y_center / size[1]
w = (box[1] - box[0]) / size[0]
h = (box[3] - box[2]) / size[1]
return (x, y, w, h)


def convert_annotation(xml_files_path, save_txt_files_path, classes):
xml_files = os.listdir(xml_files_path)
print(xml_files)
for xml_name in xml_files:
print(xml_name)
xml_file = os.path.join(xml_files_path, xml_name)
out_txt_path = os.path.join(save_txt_files_path, xml_name.split('.')[0] + '.txt')
out_txt_f = open(out_txt_path, 'w')
tree = ET.parse(xml_file)
root = tree.getroot()
size = root.find('size')
w = int(size.find('width').text)
h = int(size.find('height').text)

for obj in root.iter('object'):
difficult = obj.find('difficult').text
cls = obj.find('name').text
if cls not in classes or int(difficult) == 1:
continue
cls_id = classes.index(cls)
xmlbox = obj.find('bndbox')
b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
float(xmlbox.find('ymax').text))
# b=(xmin, xmax, ymin, ymax)
print(w, h, b)
bb = convert((w, h), b)
out_txt_f.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')


if __name__ == "__main__":
# 需要转换的类别,需要一一对应
classes1 = ['boat', 'cat']
# 2、voc格式的xml标签文件路径
xml_files1 = r'C:\Users\name\Desktop\VOC2022\Annotations'
# 3、转化为yolo格式的txt标签文件存储路径
save_txt_files1 = r'C:\Users\name\Desktop\VOC2022\label'

convert_annotation(xml_files1, save_txt_files1, classes1)

它们之间的互相装换之yolo转voc格式代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from xml.dom.minidom import Document
import os
import cv2


# def makexml(txtPath, xmlPath, picPath): # txt所在文件夹路径,xml文件保存路径,图片所在文件夹路径
def makexml(picPath, txtPath, xmlPath): # txt所在文件夹路径,xml文件保存路径,图片所在文件夹路径
"""此函数用于将yolo格式txt标注文件转换为voc格式xml标注文件
在自己的标注图片文件夹下建三个子文件夹,分别命名为picture、txt、xml
"""
dic = {'0': "boat", # 创建字典用来对类型进行转换
'1': "cat", # 此处的字典要与自己的classes.txt文件中的类对应,且顺序要一致
}
files = os.listdir(txtPath)
for i, name in enumerate(files):
xmlBuilder = Document()
annotation = xmlBuilder.createElement("annotation") # 创建annotation标签
xmlBuilder.appendChild(annotation)
txtFile = open(txtPath + name)
txtList = txtFile.readlines()
img = cv2.imread(picPath + name[0:-4] + ".jpg")
Pheight, Pwidth, Pdepth = img.shape

folder = xmlBuilder.createElement("folder") # folder标签
foldercontent = xmlBuilder.createTextNode("driving_annotation_dataset")
folder.appendChild(foldercontent)
annotation.appendChild(folder) # folder标签结束

filename = xmlBuilder.createElement("filename") # filename标签
filenamecontent = xmlBuilder.createTextNode(name[0:-4] + ".jpg")
filename.appendChild(filenamecontent)
annotation.appendChild(filename) # filename标签结束

size = xmlBuilder.createElement("size") # size标签
width = xmlBuilder.createElement("width") # size子标签width
widthcontent = xmlBuilder.createTextNode(str(Pwidth))
width.appendChild(widthcontent)
size.appendChild(width) # size子标签width结束

height = xmlBuilder.createElement("height") # size子标签height
heightcontent = xmlBuilder.createTextNode(str(Pheight))
height.appendChild(heightcontent)
size.appendChild(height) # size子标签height结束

depth = xmlBuilder.createElement("depth") # size子标签depth
depthcontent = xmlBuilder.createTextNode(str(Pdepth))
depth.appendChild(depthcontent)
size.appendChild(depth) # size子标签depth结束

annotation.appendChild(size) # size标签结束

for j in txtList:
oneline = j.strip().split(" ")
object = xmlBuilder.createElement("object") # object 标签
picname = xmlBuilder.createElement("name") # name标签
namecontent = xmlBuilder.createTextNode(dic[oneline[0]])
picname.appendChild(namecontent)
object.appendChild(picname) # name标签结束

pose = xmlBuilder.createElement("pose") # pose标签
posecontent = xmlBuilder.createTextNode("Unspecified")
pose.appendChild(posecontent)
object.appendChild(pose) # pose标签结束

truncated = xmlBuilder.createElement("truncated") # truncated标签
truncatedContent = xmlBuilder.createTextNode("0")
truncated.appendChild(truncatedContent)
object.appendChild(truncated) # truncated标签结束

difficult = xmlBuilder.createElement("difficult") # difficult标签
difficultcontent = xmlBuilder.createTextNode("0")
difficult.appendChild(difficultcontent)
object.appendChild(difficult) # difficult标签结束

bndbox = xmlBuilder.createElement("bndbox") # bndbox标签
xmin = xmlBuilder.createElement("xmin") # xmin标签
mathData = int(((float(oneline[1])) * Pwidth + 1) - (float(oneline[3])) * 0.5 * Pwidth)
xminContent = xmlBuilder.createTextNode(str(mathData))
xmin.appendChild(xminContent)
bndbox.appendChild(xmin) # xmin标签结束

ymin = xmlBuilder.createElement("ymin") # ymin标签
mathData = int(((float(oneline[2])) * Pheight + 1) - (float(oneline[4])) * 0.5 * Pheight)
yminContent = xmlBuilder.createTextNode(str(mathData))
ymin.appendChild(yminContent)
bndbox.appendChild(ymin) # ymin标签结束

xmax = xmlBuilder.createElement("xmax") # xmax标签
mathData = int(((float(oneline[1])) * Pwidth + 1) + (float(oneline[3])) * 0.5 * Pwidth)
xmaxContent = xmlBuilder.createTextNode(str(mathData))
xmax.appendChild(xmaxContent)
bndbox.appendChild(xmax) # xmax标签结束

ymax = xmlBuilder.createElement("ymax") # ymax标签
mathData = int(((float(oneline[2])) * Pheight + 1) + (float(oneline[4])) * 0.5 * Pheight)
ymaxContent = xmlBuilder.createTextNode(str(mathData))
ymax.appendChild(ymaxContent)
bndbox.appendChild(ymax) # ymax标签结束

object.appendChild(bndbox) # bndbox标签结束

annotation.appendChild(object) # object标签结束

f = open(xmlPath + name[0:-4] + ".xml", 'w')
xmlBuilder.writexml(f, indent='\t', newl='\n', addindent='\t', encoding='utf-8')
f.close()

if __name__ == "__main__":
picPath = "C:/Users/name/Desktop/VOC2022/JPEGImage/" # 图片所在文件夹路径,后面的/一定要带上
txtPath = "C:/Users/name/Desktop/VOC2022/yolo/" # txt所在文件夹路径,后面的/一定要带上
xmlPath = "C:/Users/name/Desktop/VOC2022/Annotations1/" # xml文件保存路径,后面的/一定要带上
makexml(picPath, txtPath, xmlPath)

1.2.3 划分训练集和验证集

在做深度学习目标检测模型训练的时候,首先是要获取数据集,然后再对数据集进行标注。然后再把标注完的数据集划分为训练集和验证集,这样更加方便模型的训练和测试。首先上划分数据集的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import os, random, shutil


def moveimg(fileDir, tarDir):
pathDir = os.listdir(fileDir) # 取图片的原始路径
filenumber = len(pathDir)
rate = 0.1 # 自定义抽取图片的比例,比方说100张抽10张,那就是0.1
picknumber = int(filenumber * rate) # 按照rate比例从文件夹中取一定数量图片
sample = random.sample(pathDir, picknumber) # 随机选取picknumber数量的样本图片
print(sample)
for name in sample:
shutil.move(fileDir + name, tarDir + "\\" + name)
return

def movelabel(file_list, file_label_train, file_label_val):
for i in file_list:
if i.endswith('.jpg'):
# filename = file_label_train + "\\" + i[:-4] + '.xml' # 可以改成xml文件将’.txt‘改成'.xml'就可以了
filename = file_label_train + "\\" + i[:-4] + '.txt' # 可以改成xml文件将’.txt‘改成'.xml'就可以了
if os.path.exists(filename):
shutil.move(filename, file_label_val)
print(i + "处理成功!")



if __name__ == '__main__':
fileDir = r"C:\Users\86159\Desktop\hat\JPEGImages" + "\\" # 源图片文件夹路径
tarDir = r'C:\Users\86159\Desktop\hat\JPEGImages_val' # 图片移动到新的文件夹路径
moveimg(fileDir, tarDir)
file_list = os.listdir(tarDir)
file_label_train = r"C:\Users\name\Desktop\hat\Annotations_yolo" # 源图片标签路径
file_label_val = r"C:\Users\name\Desktop\hat\Annotations_val" # 标签
# 移动到新的文件路径
movelabel(file_list, file_label_train, file_label_val)

如上的代码的原理是将一个文件夹里面的图片按一定的比例(可以自己设定,修改第7行中的rate的值就可以了)抽取照片放入到一个新的文件夹里面(需要自己新建一个文件夹)。这样源文件夹就变成了训练集,新建的为验证集。然后代码会将抽取照片对应的标签文件放入到一个新建的文件夹中(该文件夹需要新建)。这样我们就有图片的训练集和验证集,还有其对应的标签文件。使用同样的代码还可以将数据集多划分一个测试集出来,这样方便测试用。

最后将得到的数据集和训练集放在一起,文件目录结构树如下所示:

1
2
3
4
5
6
7
├─VOCdevkit
images├──
├──train
├──val
labels├──
├──train
├──val
1.2.4获得预训练权重文件

一般为了缩短网络的训练时间,并达到更好的精度,我们一般加载预训练权重进行网络的训练。而yolov5的5.0版本给我们提供了几个预训练权重,我们可以对应我们不同的需求选择不同的版本的预训练权重。通过如下的图可以获得权重的名字和大小信息,可以预料的到,预训练权重越大,训练出来的精度就会相对来说越高,但是其检测的速度就会越慢。预训练权重可以前往仓库地址进行下载,本次训练自己的数据集用的预训练权重为yolov5s.pt。

1.3 训练自己的模型
1.3.1 修改数据配置文件

预训练模型和数据集都准备好了,就可以开始训练自己的yolov5目标检测模型了,训练目标检测模型需要修改两个yaml文件中的参数。一个是data目录下的相应的yaml文件,一个是model目录文件下的相应的yaml文件。

修改data目录下的相应的yaml文件。找到目录下的voc.yaml文件,将该文件复制一份,将复制的文件重命名,最好和项目相关,这样方便后面操作。我这里修改为hat.yaml。该项目是对安全帽的识别。

打开这个文件并修改其中的参数,首先将其中的那一行代码注释掉(我已经注释掉了),如果不注释这行代码训练的时候会报错;而后需要将训练和测试的数据集的路径填上(最好要填绝对路径,有时候由目录结构的问题会莫名奇妙的报错);之后设置需要检测的类别数,我这里是识别安全帽和人,所以这里填写2;最后填写需要识别的类别的名字(必须是英文,否则会乱码识别不出来)。到这里和data目录下的yaml文件就修改好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# PASCAL VOC dataset http://host.robots.ox.ac.uk/pascal/VOC/
# Train command: python train.py --data voc.yaml
# Default dataset location is next to /yolov5:
# /parent_folder
# /VOC
# /yolov5


# download command/URL (optional)
# download: bash data/scripts/get_voc.sh 注释掉

# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/]
train: VOCdevkit/images/train # 16551 images
val: VOCdevkit/images/val # 4952 images

# number of classes
nc: 2

# class names
names: [ 'hat', 'person' ]
1.3.2 修改模型配置文件

由于该项目使用的是yolov5s.pt这个预训练权重,所以要使用models目录下的yolov5s.yaml文件中的相应参数(因为不同的预训练权重对应着不同的网络层数,所以用错预训练权重会报错)。同上修改data目录下的yaml文件一样,我们最好将yolov5s.yaml文件复制一份,然后将其重命名,我将其重命名为yolov5_hat.yaml。

打开yolov5_hat.yaml文件只需要修改下面中的数字就好了,这里是识别两个类别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# parameters
nc: 2 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # layer channel multiple

# anchors
anchors:
- [10,13, 16,30, 33,23] # P3/8
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32

# YOLOv5 backbone
backbone:
# [from, number, module, args]
[[-1, 1, Focus, [64, 3]], # 0-P1/2
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8
[-1, 9, C3, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16
[-1, 9, C3, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
[-1, 1, SPP, [1024, [5, 9, 13]]],
[-1, 3, C3, [1024, False]], # 9
]

# YOLOv5 head
head:
[[-1, 1, Conv, [512, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 6], 1, Concat, [1]], # cat backbone P4
[-1, 3, C3, [512, False]], # 13

[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 4], 1, Concat, [1]], # cat backbone P3
[-1, 3, C3, [256, False]], # 17 (P3/8-small)

[-1, 1, Conv, [256, 3, 2]],
[[-1, 14], 1, Concat, [1]], # cat head P4
[-1, 3, C3, [512, False]], # 20 (P4/16-medium)

[-1, 1, Conv, [512, 3, 2]],
[[-1, 10], 1, Concat, [1]], # cat head P5
[-1, 3, C3, [1024, False]], # 23 (P5/32-large)

[[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]

至此,相应的配置参数就修改好了。

1.3.3 训练自己的模型并启用tensorboard查看参数

如果上面的数据集和两个yaml文件的参数都修改好了的话,就可以开始yolov5的训练了。首先我们找到train.py这个py文件。

然后找到主函数的入口,这里面有模型的主要参数。模型的主要参数解析如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
if __name__ == '__main__':
"""
opt模型主要参数解析:
--weights:初始化的权重文件的路径地址
--cfg:模型yaml文件的路径地址
--data:数据yaml文件的路径地址
--hyp:超参数文件路径地址
--epochs:训练轮次
--batch-size:喂入批次文件的多少
--img-size:输入图片尺寸
--rect:是否采用矩形训练,默认False
--resume:接着打断训练上次的结果接着训练
--nosave:不保存模型,默认False
--notest:不进行test,默认False
--noautoanchor:不自动调整anchor,默认False
--evolve:是否进行超参数进化,默认False
--bucket:谷歌云盘bucket,一般不会用到
--cache-images:是否提前缓存图片到内存,以加快训练速度,默认False
--image-weights:使用加权图像选择进行训练
--device:训练的设备,cpu;0(表示一个gpu设备cuda:0);0,1,2,3(多个gpu设备)
--multi-scale:是否进行多尺度训练,默认False
--single-cls:数据集是否只有一个类别,默认False
--adam:是否使用adam优化器
--sync-bn:是否使用跨卡同步BN,在DDP模式使用
--local_rank:DDP参数,请勿修改
--workers:最大工作核心数
--project:训练模型的保存位置
--name:模型保存的目录名称
--exist-ok:模型目录是否存在,不存在就创建
"""
parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default='yolov5s.pt', help='initial weights path')
parser.add_argument('--cfg', type=str, default='', help='model.yaml path')
parser.add_argument('--data', type=str, default='data/coco128.yaml', help='data.yaml path')
parser.add_argument('--hyp', type=str, default='data/hyp.scratch.yaml', help='hyperparameters path')
parser.add_argument('--epochs', type=int, default=300)
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs')
parser.add_argument('--img-size', nargs='+', type=int, default=[640, 640], help='[train, test] image sizes')
parser.add_argument('--rect', action='store_true', help='rectangular training')
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
parser.add_argument('--notest', action='store_true', help='only test final epoch')
parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
parser.add_argument('--evolve', action='store_true', help='evolve hyperparameters')
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
parser.add_argument('--cache-images', action='store_true', help='cache images for faster training')
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer')
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify')
parser.add_argument('--workers', type=int, default=8, help='maximum number of dataloader workers')
parser.add_argument('--project', default='runs/train', help='save to project/name')
parser.add_argument('--entity', default=None, help='W&B entity')
parser.add_argument('--name', default='exp', help='save to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
parser.add_argument('--quad', action='store_true', help='quad dataloader')
parser.add_argument('--linear-lr', action='store_true', help='linear LR')
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
parser.add_argument('--upload_dataset', action='store_true', help='Upload dataset as W&B artifact table')
parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval for W&B')
parser.add_argument('--save_period', type=int, default=-1, help='Log model after every "save_period" epoch')
parser.add_argument('--artifact_alias', type=str, default="latest", help='version of dataset artifact to be used')
opt = parser.parse_args()

训练自己的模型需要修改如下几个参数就可以训练了。首先将weights权重的路径填写到对应的参数里面,然后将修好好的models模型的yolov5s.yaml文件路径填写到相应的参数里面,最后将data数据的hat.yaml文件路径填写到相对于的参数里面。这几个参数就必须要修改的参数。

1
2
3
parser.add_argument('--weights', type=str, default='weights/yolov5s.pt', help='initial weights path')
parser.add_argument('--cfg', type=str, default='models/yolov5s_hat.yaml', help='model.yaml path')
parser.add_argument('--data', type=str, default='data/hat.yaml', help='data.yaml path')

还有几个需要根据自己的需求来更改的参数:首先是模型的训练轮次,这里是训练的300轮。

1
parser.add_argument('--epochs', type=int, default=300)

其次是输入图片的数量和工作的核心数,倘若出现CUDA显存溢出,则需要调小数值。

1
2
parser.add_argument('--batch-size', type=int, default=8, help='total batch size for all GPUs')
parser.add_argument('--workers', type=int, default=8, help='maximum number of dataloader workers')

以上都设置好了就可以训练了。但是pycharm的用户可能会出现提示WinError 1455页面文件太小无法操作的报错。这是说明虚拟内存不够了。前往utils/datasets.py文件中,将里面的第81行里面的参数nw改完0就可以了。

至此,就可以运行train.py函数训练自己的模型了。

1.3.4 启用tensorboard查看训练过程

yolov5里面有写好的tensorbord函数,可以运行命令就可以调用tensorbord,然后查看tensorbord了。首先打开pycharm的命令控制终端,输入如下命令,就会出现一个网址地址,将那行网址复制下来到浏览器打开就可以看到训练的过程了。

1
tensorboard --logdir=runs/train

如果模型已经训练好了,但是我们还想用tensorbord查看此模型的训练过程,就需要输入如下的命令。就可以看到模型的训练结果了。

1
tensorboard --logdir=runs
1.3.5 推理测试

等到数据训练好了以后,就会在主目录下产生一个run文件夹,在run/train/exp/weights目录下会产生两个权重文件,一个是最后一轮的权重文件,一个是最好的权重文件,一会我们就要利用这个最好的权重文件来做推理测试。除此以外还会产生一些验证文件的图片等一些文件。

找到主目录下的detect.py文件,打开该文件。然后找到主函数的入口,这里面有模型的主要参数。模型的主要参数解析如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
f __name__ == '__main__':
"""
--weights:权重的路径地址
--source:测试数据,可以是图片/视频路径,也可以是'0'(电脑自带摄像头),也可以是rtsp等视频流
--output:网络预测之后的图片/视频的保存路径
--img-size:网络输入图片大小
--conf-thres:置信度阈值
--iou-thres:做nms的iou阈值
--device:是用GPU还是CPU做推理
--view-img:是否展示预测之后的图片/视频,默认False
--save-txt:是否将预测的框坐标以txt文件形式保存,默认False
--classes:设置只保留某一部分类别,形如0或者0 2 3
--agnostic-nms:进行nms是否也去除不同类别之间的框,默认False
--augment:推理的时候进行多尺度,翻转等操作(TTA)推理
--update:如果为True,则对所有模型进行strip_optimizer操作,去除pt文件中的优化器等信息,默认为False
--project:推理的结果保存在runs/detect目录下
--name:结果保存的文件夹名称
"""
parser = argparse.ArgumentParser()
parser.add_argument('--weights', nargs='+', type=str, default='yolov5s.pt', help='model.pt path(s)')
parser.add_argument('--source', type=str, default='data/images', help='source') # file/folder, 0 for webcam
parser.add_argument('--img-size', type=int, default=640, help='inference size (pixels)')
parser.add_argument('--conf-thres', type=float, default=0.25, help='object confidence threshold')
parser.add_argument('--iou-thres', type=float, default=0.45, help='IOU threshold for NMS')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--view-img', action='store_true', help='display results')
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
parser.add_argument('--nosave', action='store_true', help='do not save images/videos')
parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --class 0, or --class 0 2 3')
parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
parser.add_argument('--augment', action='store_true', help='augmented inference')
parser.add_argument('--update', action='store_true', help='update all models')
parser.add_argument('--project', default='runs/detect', help='save results to project/name')
parser.add_argument('--name', default='exp', help='save results to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
opt = parser.parse_args()

这里需要将刚刚训练好的最好的权重传入到推理函数中去。然后就可以对图像视频进行推理了。

1
parser.add_argument('--weights', nargs='+', type=str, default='runs/train/exp/weights/best.pt', help='model.pt path(s)')

对图片进行测试推理,将如下参数修改成图片的路径,然后运行detect.py就可以进行测试了。

1
parser.add_argument('--source', type=str, default='000295.jpg', help='source')

推理测试结束以后,在run下面会生成一个detect目录,推理结果会保存在exp目录下。

利用摄像头进行测试只需将路径改写为0就好了。但是有可能会出现以下报错:提示TypeError: argument of type 'int' is not iterable. 解决方法:首先找到datasets.py这个py文件。 打开文件,找到第279行代码,给两个url参数加上str就可以了,如下所示,就可以完美运行电脑的摄像头了。

1
2
3
4
if 'youtube.com/' in str(url) or 'youtu.be/' in str(url):  # if source is YouTube video
check_requirements(('pafy', 'youtube_dl'))
import pafy
url = pafy.new(url).getbest(preftype="mp4").url

2. 人体姿态估计与动作捕捉

近日,原 YOLOv3/YOLOv4/Scaled-YOLOv4 的团队推出了 YOLOv7,源码地址:https://github.com/WongKinYiu/yolov7,同样是基于 pytorch 框架,对应的论文地址:YOLOv7: Trainable bag-of-freebies sets new state-of-the-art for real-time object detectors,感兴趣的童鞋可以去读一读。

据官方的介绍,YOLOv7 是当前实时目标检测算法的 State Of The Art 也就是 SOTA

当然,开始之前我们还是要用Anaconda创建虚拟环境,下载PyTorch

以下代码即为验证PyTorch环境。

1
2
3
4
5
6
7
8
9
(pytorch1.6) PS C:\Windows\system32> python
Python 3.7.9 (default, Aug 31 2020, 17:10:11) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> torch.__version__
'1.6.0+cu101'
>>> torch.cuda.is_available()
True
>>>

首先,我们下载官方源码

1
2
git clone https://github.com/WongKinYiu/yolov7.git
cd yolov7

然后安装依赖

1
2
# 如果前面安装了pytorch,这里就将requirements.txt中的pytorch注释掉
pip install -r requirements.txt

依赖安装结束后,可以执行

1
python detect.py

代码执行过程中会去下载模型文件 yolov7.pt,下载成功后,会去检测 inference/images 目录中的图片,这里只有一张图片,结束后,检测结果为输出路径中图片。

当然也可以指定数据源,带上参数 --source

1
python detect.py --source bus.jpg

如果是视频文件检测,--source 参数后面直接跟上视频文件,摄像头检测的话,--source 参数后面跟上摄像头的 id,如 0、1 …;网络流媒体的话,也是支持 httprtsprtmp 等协议

YOLOv7 除了实现目标检测,它还会带来人体关键点检测和实例分割这2个重要功能

目前人体关键点检测功能基本完成,我们来看看示例,在 tools/keypoint.ipynb,在 notebook 中可以直接运行。

如果需要将 ipynb 文件转成 python 文件,就执行(在YOLO v7项目根路径下执行)

1
jupyter nbconvert --to python tools/keypoint.ipynb

将生成的 tools/keypoint.py 拷贝到源码根目录下,然后修改文件的最后部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
output = non_max_suppression_kpt(output, 0.25, 0.65, nc=model.yaml['nc'], nkpt=model.yaml['nkpt'], kpt_label=True)
output = output_to_keypoint(output)
nimg = image[0].permute(1, 2, 0) * 255
nimg = nimg.cpu().numpy().astype(np.uint8)
nimg = cv2.cvtColor(nimg, cv2.COLOR_RGB2BGR)
for idx in range(output.shape[0]):
plot_skeleton_kpts(nimg, output[idx, 7:].T, 3)

# 将结果保存下来
cv2.imwrite('person_keypoint.jpg', nimg)
# In[5]:

#get_ipython().run_line_magic('matplotlib', 'inline')
#plt.figure(figsize=(8,8))
#plt.axis('off')
#plt.imshow(nimg)
#plt.show()

然后去下载模型文件,https://github.com/WongKinYiu/yolov7/releases/download/v0.1/yolov7-w6-pose.pt,同样放在源码根目录下,准备一张测试图片,最后,执行

1
python keypoint.py

至于视频文件或者摄像头的情况,这里稍微修改一下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import torch
import cv2
import numpy as np
import time

from torchvision import transforms
from utils.datasets import letterbox
from utils.general import non_max_suppression_kpt
from utils.plots import output_to_keypoint, plot_skeleton_kpts

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
weigths = torch.load('yolov7-w6-pose.pt')
model = weigths['model']
model = model.half().to(device)
_ = model.eval()

cap = cv2.VideoCapture('gym_test.mp4')
if (cap.isOpened() == False):
print('open failed.')

# 分辨率
frame_width = int(cap.get(3))
frame_height = int(cap.get(4))

# 图片缩放
vid_write_image = letterbox(cap.read()[1], (frame_width), stride=64, auto=True)[0]
resize_height, resize_width = vid_write_image.shape[:2]

# 保存结果视频
out = cv2.VideoWriter("result_keypoint.mp4",
cv2.VideoWriter_fourcc(*'mp4v'), 30,
(resize_width, resize_height))

frame_count = 0
total_fps = 0

while(cap.isOpened):
ret, frame = cap.read()
if ret:
orig_image = frame
image = cv2.cvtColor(orig_image, cv2.COLOR_BGR2RGB)
image = letterbox(image, (frame_width), stride=64, auto=True)[0]
image_ = image.copy()
image = transforms.ToTensor()(image)
image = torch.tensor(np.array([image.numpy()]))
image = image.to(device)
image = image.half()

start_time = time.time()
with torch.no_grad():
output, _ = model(image)
end_time = time.time()

# 计算fps
fps = 1 / (end_time - start_time)
total_fps += fps
frame_count += 1

output = non_max_suppression_kpt(output, 0.25, 0.65, nc=model.yaml['nc'], nkpt=model.yaml['nkpt'], kpt_label=True)
output = output_to_keypoint(output)
nimg = image[0].permute(1, 2, 0) * 255
nimg = nimg.cpu().numpy().astype(np.uint8)
nimg = cv2.cvtColor(nimg, cv2.COLOR_RGB2BGR)
for idx in range(output.shape[0]):
plot_skeleton_kpts(nimg, output[idx, 7:].T, 3)

# 显示fps
cv2.putText(nimg, f"{fps:.3f} FPS", (15, 30), cv2.FONT_HERSHEY_SIMPLEX,
1, (0, 255, 0), 2)

# 显示结果并保存
cv2.imshow('image', nimg)
out.write(nimg)

# 按q退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
else:
break

# 资源释放
cap.release()
cv2.destroyAllWindows()

# 计算平均fps
avg_fps = total_fps / frame_count
print(f"Average FPS: {avg_fps:.3f}")

如果执行过程中报错,提示utils/plots.py的442行output_to_keypoint函数的numpy()出现错误,修改方法:

就需要去修改下 utils/plots.py,将.cpu().numpy()修改为.cpu().detach().numpy()。

这是因为 Tensor 变量带有梯度,如果直接转换为 numpy 数据将会破坏计算图,这里不需要保留梯度信息,可以在变量转换之前调用 detach()

最后,实例分割的模型还没有放出,我们持续关注。

3. 目标追踪

还是老方法,需要安装从YOLO v7仓库中下载文件到本地。

git clone https://github.com/WongKinYiu/yolov7,然后下载YOLO v7模型文件到项目根目录。然后前往YOLO v7 目标追踪仓库地址下载文件到本地。两个仓库的文件可以是并列关系。而后进行git clone https://github.com/haroonshakeel/yolov7-object-tracking.git,下载完成之后,进入目标追踪的项目文件夹:E:\PyCharm_SPACE\Tracking_yolov7\yolov7-object-tracking,复制其中的多项文件:detect_or_track.py requirements.txt requirements_gpu.txt sort.py street.mp4这五个文件,覆盖粘贴给原YOLO v7项目文件夹内。

还是老样子创建conda虚拟环境:

1
2
3
4
5
6
conda create -n yolov7_tracking python=3.9
conda activate yolov7_tracking
pip install -r requirements.txt
pip install -r requirements_gpu.txt
#验证CUDA
#CMD下,python import torch torch.cuda.is_available() exit()

然后在控制台输入下列命令:(需要改动的即为detect_or_track.py文件)

1
python detect_or_track.py --weights yolov7.pt --no-trace --view-img --source street.mp4 --show-fps --track --show-track --unique-track-color

即可查看效果啦。

4.其他

参考链接:

1
2
3
4
5
6
7
8
https://space.bilibili.com/299876864
https://blog.csdn.net/didiaopao/article/details/119954291
https://xugaoxiang.com/2022/07/21/yolov7/
https://xugaoxiang.com/2020/11/11/python-module-argparse/
https://www.bilibili.com/video/BV1wd4y1T7EC/
https://github.com/haroonshakeel/yolov7-object-tracking
https://www.bilibili.com/video/BV1f14y1V7jC/
https://www.youtube.com/watch?v=K_OGcfXwskc&ab_channel=TheCodingBug