
基于无人机影像的小目标车辆检测及量化训练方案
本项目基于PP-YOLOE-SOD实现了基于无人机影像的小目标车辆检测,并比较了大尺度和切图两种涨点方案,切图后模型在子图上验证约有6.7%的精度提升。
★★★ 本文源自AlStudio社区精品项目,【点击此处】查看更多精品内容 >>>
本项目基于PP-YOLOE-SOD实现了基于无人机影像的小目标车辆检测,并比较了大尺度和切图两种涨点方案,具体结果如下:
模型名称 | IoU=0.50:0.95 | FPS |
---|---|---|
ppyoloe_plus_sod_crn_l_80e_coco | 0.643 | 17.378 |
ppyoloe_plus_sod_crn_l_largesize_80e_visdrone | 0.633 | 4.899 |
ppyoloe_plus_sod_crn_l_80e_coco | 0.710 | 23.057 |
切图后模型在子图上验证约有6.7%的精度提升,并提供了一种模型量化的方案。
一、项目背景
无人机的使用始于21世纪初的军事目的,但随着时间的推移,它们已开始用于其他领域。交通管理中最具挑战性的问题之一是复杂的场景,例如在交通环形路口和交叉路口。在这些复杂的场景中,无人机可用于与自动驾驶汽车或交通基础设施相结合,为它们提供没有这种空中视野就无法获得的信息。
智能交通领域的其他工作包括RGB热图像的语义分割以及突出物体的识别。在这些任务中,传统的RGB图像与热图像以及深度图像相结合,以利用所有可用的环境信息。
二、数据集介绍
这是一个从无人机 (UAV) 获取的西班牙道路交通图像数据集,旨在用于训练深度学习模型以提高道路交通视觉和管理系统的性能。
数据集由 15070 张 png 格式的图像组成,并附有对应txt格式的标注文件。这些图像在城市道路的六个不同位置拍摄,标记有155328辆汽车,包括汽车(137602辆)和摩托车(17726辆)。
Scenes | Sequences | Frames | Targets | Cars | Motorbikes |
---|---|---|---|---|---|
Regional road | sec1 | 4,500 | 24,858 | 14,577 | 10,281 |
Urban intersection | sec9, seca, secb, secc | 2,462 | 10,759 | 10,759 | 0 |
Rural road | sec7 | 1,292 | 746 | 746 | 0 |
Split roundabout | sec8 | 3,107 | 3,107 | 3,107 | 0 |
Roundabout (far) | sec2, sec3 | 1,814 | 71,819 | 64,844 | 6,975 |
Roundabout (near) | sec4, sec5, sec6 | 3,997 | 44,039 | 43,569 | 470 |
Total | 15,070 | 155,328 | 137,602 | 17,726 |
标注文件包含以下内容:
- Object class:介于 0 和 N-1 之间的整数。本数据集包含两个类别:0.Cars,1.Motorcycles。
- x, y:相对于包含标记对象的矩形中心的十进制值。它们的范围为 [0.0 到 1.0]。
- Weight、Height:相对于包含标记对象的矩形的宽度和高度的十进制值。它们的范围为 [0.0 到 1.0]。
部分数据如下所示:
三、数据预处理
Step01: 解压数据集到/home/aistudio/work目录下。
!unzip /home/aistudio/data/data203056/archive.zip -d /home/aistudio/work/
Step02: 分隔开图片和标注文件。
工作目录:/home/aistudio/work。
- Vehicle:处理后的数据集的存放位置。
- JPEGImages:Vehicle目录下的子文件夹,用于存放数据集中的图片。
- txt_label:Vehicle目录下的子文件夹,用于存放数据集中的原有的txt格式的标注文件。
%cd /home/aistudio/work/
!mkdir Vehicle
%cd Vehicle
!mkdir JPEGImages
!mkdir txt_label
!mv /home/aistudio/work/dataset/sec1/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/sec1/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/sec2/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/sec2/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/sec3/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/sec3/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/sec4/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/sec4/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/sec5/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/sec5/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/sec6/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/sec6/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/sec7/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/sec7/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/sec8/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/sec8/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/sec9/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/sec9/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/seca/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/seca/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/secb/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/secb/*.txt /home/aistudio/work/Vehicle/txt_label/
!mv /home/aistudio/work/dataset/secc/*.png /home/aistudio/work/Vehicle/JPEGImages/
!mv /home/aistudio/work/dataset/secc/*.txt /home/aistudio/work/Vehicle/txt_label/
Step03: 将txt格式标注文件转换成可供训练的xml格式标注文件。
- Annotations:Vehicle目录下的子文件夹,用于存放转换后的xml格式的标注文件。
!mkdir Annotations
from xml.dom.minidom import Document
import os
import cv2
import shutil
def makexml(picPath, txtPath, xmlPath):
# 标签映射
dic = {'0': "Cars", '1': "Motorcycles"}
files = os.listdir(txtPath)
for i, name in enumerate(files):
if not os.path.getsize(txtPath + name):
print(name, " is empty!")
shutil.move(txtPath + name, "/home/aistudio/work/Vehicle/EmptyTxt/" + name)
shutil.move(picPath + name[0:-4] + ".png", "/home/aistudio/work/Vehicle/UnLabeled/" + name[0:-4] + ".png")
continue
if name == ".ipynb_checkpoints":
continue
xmlBuilder = Document()
annotation = xmlBuilder.createElement("annotation") # 创建annotation标签
xmlBuilder.appendChild(annotation)
txtFile = open(txtPath + name)
txtList = txtFile.readlines()
img = cv2.imread(picPath + name[0:-4] + ".png") # .jpg/.png
Pheight, Pwidth, Pdepth = img.shape
folder = xmlBuilder.createElement("folder") # folder标签
foldercontent = xmlBuilder.createTextNode("datasetRGB")
folder.appendChild(foldercontent)
annotation.appendChild(folder)
filename = xmlBuilder.createElement("filename") # filename标签
filenamecontent = xmlBuilder.createTextNode(name[0:-4] + ".jpg")
filename.appendChild(filenamecontent)
annotation.appendChild(filename)
size = xmlBuilder.createElement("size") # size标签
width = xmlBuilder.createElement("width") # size子标签width
widthcontent = xmlBuilder.createTextNode(str(Pwidth))
width.appendChild(widthcontent)
size.appendChild(width)
height = xmlBuilder.createElement("height") # size子标签height
heightcontent = xmlBuilder.createTextNode(str(Pheight))
height.appendChild(heightcontent)
size.appendChild(height)
depth = xmlBuilder.createElement("depth") # size子标签depth
depthcontent = xmlBuilder.createTextNode(str(Pdepth))
depth.appendChild(depthcontent)
size.appendChild(depth)
annotation.appendChild(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)
pose = xmlBuilder.createElement("pose") # pose标签
posecontent = xmlBuilder.createTextNode("Unspecified")
pose.appendChild(posecontent)
object.appendChild(pose)
truncated = xmlBuilder.createElement("truncated") # truncated标签
truncatedContent = xmlBuilder.createTextNode("0")
truncated.appendChild(truncatedContent)
object.appendChild(truncated)
difficult = xmlBuilder.createElement("difficult") # difficult标签
difficultcontent = xmlBuilder.createTextNode("0")
difficult.appendChild(difficultcontent)
object.appendChild(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)
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)
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)
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)
object.appendChild(bndbox) # bndbox标签结束
annotation.appendChild(object)
f = open(xmlPath + name[0:-4] + ".xml", 'w')
print(name)
xmlBuilder.writexml(f, indent='\t', newl='\n', addindent='\t', encoding='utf-8')
f.close()
if __name__ == "__main__":
picPath = "/home/aistudio/work/Vehicle/JPEGImages/" # 图片所在文件夹路径
txtPath = "/home/aistudio/work/Vehicle/txt_label/" # txt所在文件夹路径
xmlPath = "/home/aistudio/work/Vehicle/Annotations/" # xml文件保存路径
makexml(picPath, txtPath, xmlPath)
以下代码块用于可视化xml格式标注文件中的标注结果,以判断是否准确无误。
由于数据集过大,不建议可视化全部的图片,可视化部分文件即可。
import xml.etree.ElementTree as ET
import os
import cv2
src_XML_dir = '/home/aistudio/work/Vehicle/Annotations' # xml源路径
src_IMG_dir = '/home/aistudio/work/Vehicle/JPEGImages' # IMG原路径
IMG_format = '.png' # IMG格式
out_dir = '/home/aistudio/work/output' # 输出路径
if not os.path.exists(out_dir):
os.makedirs(out_dir)
xml_file = os.listdir(src_XML_dir) # 只返回文件名称,带后缀
for each_XML in xml_file: # 遍历所有xml文件
# 读入IMG
if each_XML == ".ipynb_checkpoints":
continue
xml_FirstName = os.path.splitext(each_XML)[0]
img_save_file = os.path.join(out_dir, xml_FirstName+IMG_format)
img_src_path = os.path.join(src_IMG_dir, xml_FirstName+IMG_format)
img = cv2.imread(img_src_path)
# 解析XML
each_XML_fullPath = src_XML_dir + '/' + each_XML # 每个xml文件的完整路径
tree = ET.parse(each_XML_fullPath) # ET.parse()内要为完整相对路径
root = tree.getroot() # 类型为element
# 画框
for obj in root.findall('object'):
if obj.find('bndbox'):
if obj.find('name').text == 'Cars':
bndbox = obj.find('bndbox')
xmin = int(bndbox.find('xmin').text)
xmax = int(bndbox.find('xmax').text)
ymin = int(bndbox.find('ymin').text)
ymax = int(bndbox.find('ymax').text)
cv2.rectangle(img=img,
pt1=(xmin,ymin),
pt2=(xmax,ymax),
color=(0,0,255),
thickness=2)
if obj.find('name').text == ' Motorcycles':
bndbox = obj.find('bndbox')
xmin = int(bndbox.find('xmin').text)
xmax = int(bndbox.find('xmax').text)
ymin = int(bndbox.find('ymin').text)
ymax = int(bndbox.find('ymax').text)
cv2.rectangle(img=img,
pt1=(xmin,ymin),
pt2=(xmax,ymax),
color=(0,255,0),
thickness=2)
cv2.imwrite(filename=img_save_file, img=img)
print('保存结果{}'.format(xml_FirstName))
Step04: 单独存放空标注的图片和标注文件。
在上一步中发现存在部分未标注的图片,由于PaddleDetection暂不支持负样本训练,因此我们需要在这一步中将未标注图片和对应的标注文件(此处虽然有对应的标注文件,但不存在有效的标注信息)单独取出。
- EmptyTxt:Vehicle目录下的子文件夹,用于未标注图片对应的标注文件。
- UnLabeled:Vehicle目录下的子文件夹,用于未标注的图片。
!mkdir EmptyTxt
!mkdir UnLabeled
import os
XmlDir = r"/home/aistudio/work/Vehicle/Annotations"
JpgDir = r"/home/aistudio/work/Vehicle/JPEGImages"
NoXml = []
NoJpg = []
for root, dirs, files in os.walk(JpgDir):
for file in files:
if file == ".ipynb_checkpoints":
continue
if file[-1] == 'g':
if os.path.exists(XmlDir + "/"+file[:-3] + "xml") is False:
NoXml.append(JpgDir+"/"+file)
for root, dirs, files in os.walk(XmlDir):
for file in files:
if file == ".ipynb_checkpoints":
continue
if file[-1] == 'l':
if os.path.exists(JpgDir + "/"+file[:-3] + "png") is False:
NoJpg.append(XmlDir+"/"+file)
if len(NoXml) == 0:
print("All jpg are labeled")
else:
print("%d unlabeled" % len(NoXml))
print(NoXml)
for xml in NoXml:
os.remove(xml)
if len(NoJpg) == 0:
print("All xml have a jpg")
else:
print("%d xmls have no jpg" % len(NoJpg))
print(NoJpg)
for jpg in NoJpg:
os.remove(jpg)
Step05: 划分数据集。
首先安装PaddleX。
!pip install paddlex
通过split_dataset这个API按照0.75:0.15:0.1的比例划分训练集、验证集和测试集。
!paddlex --split_dataset --format VOC --dataset_dir /home/aistudio/work/Vehicle --val_value 0.15 --test_value 0.1
Step06: 将VOC格式数据集转换成COCO格式数据集。
首先,安装PaddleDetection。
# 克隆PaddleDetection仓库
%cd /home/aistudio/
#!git clone https://github.com/PaddlePaddle/PaddleDetection.git
# 安装其他依赖
%cd PaddleDetection
!pip install -r requirements.txt --user
# 编译安装paddledet
!python setup.py install
然后,将数据集移动到/home/aistudio/PaddleDetection/dataset目录下。
!mv /home/aistudio/work/Vehicle /home/aistudio/PaddleDetection/dataset
最后,通过运行tools/x2coco.py将VOC格式数据集转换成COCO格式数据集。
通过tools/x2coco.py将VOC格式数据集转换成COCO格式数据集的时候,总是将png后缀的图片写入成jpg格式。经查找,并未找到对应的配置参数进行修改。因此,我决定从源码入手进行如下修改:
!python tools/x2coco.py \
--dataset_type voc \
--voc_anno_dir dataset/Vehicle/ \
--voc_anno_list dataset/Vehicle/train_list.txt \
--voc_label_list dataset/Vehicle/labels.txt \
--voc_out_name dataset/Vehicle/voc_train.json
!python tools/x2coco.py \
--dataset_type voc \
--voc_anno_dir dataset/Vehicle/ \
--voc_anno_list dataset/Vehicle/val_list.txt \
--voc_label_list dataset/Vehicle/labels.txt \
--voc_out_name dataset/Vehicle/voc_val.json
!python tools/x2coco.py \
--dataset_type voc \
--voc_anno_dir dataset/Vehicle/ \
--voc_anno_list dataset/Vehicle/test_list.txt \
--voc_label_list dataset/Vehicle/labels.txt \
--voc_out_name dataset/Vehicle/voc_test.json
四、数据集分析
运行该代码块,我们可以看到:
- 图片尺寸分为两种:[1080,1920]和[720,1280];
- 训练集共有2793张图片,包含29005个标注物体;
- 标签类别为2,Cars/Motorcycles;
- Cars:MotoCycles = 88.7%:11.3%。
import os
import json
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
myfont = FontProperties(fname=r"/home/aistudio/work/times.ttf", size=12)
plt.rcParams['figure.figsize'] = (12, 12)
plt.rcParams['font.family']= myfont.get_family()
plt.rcParams['font.sans-serif'] = myfont.get_name()
plt.rcParams['axes.unicode_minus'] = False
def generate_anno_eda(dataset_path, anno_file):
with open(os.path.join(dataset_path, anno_file)) as f:
anno = json.load(f)
print('标签类别:', anno['categories'])
print('类别数量:', len(anno['categories']))
print('训练集图片数量:', len(anno['images']))
print('训练集标签数量:', len(anno['annotations']))
total=[]
for img in anno['images']:
hw = (img['height'],img['width'])
total.append(hw)
unique = set(total)
for k in unique:
print('长宽为(%d,%d)的图片数量为:'%k,total.count(k))
ids=[]
images_id=[]
for i in anno['annotations']:
ids.append(i['id'])
images_id.append(i['image_id'])
print('训练集图片数量:', len(anno['images']))
print('unique id 数量:', len(set(ids)))
print('unique image_id 数量', len(set(images_id)))
# 创建类别标签字典
category_dic=dict([(i['id'],i['name']) for i in anno['categories']])
counts_label=dict([(i['name'],0) for i in anno['categories']])
for i in anno['annotations']:
counts_label[category_dic[i['category_id']]] += 1
label_list = counts_label.keys() # 各部分标签
print('标签列表:', label_list)
size = counts_label.values() # 各部分大小
color = ['#FFB6C1', '#D8BFD8'] # 各部分颜色
# explode = [0.05, 0, 0] # 各部分突出值
patches, l_text, p_text = plt.pie(size, labels=label_list, colors=color, labeldistance=1.1, autopct="%1.1f%%", shadow=False, startangle=90, pctdistance=0.6, textprops={'fontproperties':myfont})
plt.axis("equal") # 设置横轴和纵轴大小相等,这样饼才是圆的
plt.legend(prop=myfont)
plt.show()
# 分析训练集数据
generate_anno_eda('/home/aistudio/PaddleDetection/dataset/Vehicle', 'voc_train.json')
通过运行tools/box_distribution.py我们可以统计数据集分布。
- Suggested reg_range[1] is # DFL算法中推荐值,在 PP-YOLOE-SOD 模型的配置文件的head中设置为此值,效果最佳
- Mean of all img_w is 1644.3394199785178 # 原图宽的平均值
- Mean of all img_h is 924.9409237379163 # 原图高的平均值
- Median of ratio_w is 0.040345982142857145 # 标注框的宽与原图宽的比例的中位数
- Median of ratio_h is 0.044444444444444446 # 标注框的高与原图高的比例的中位数
- all_img with box: 2793 # 数据集图片总数(排除无框或空标注的图片)
- all_ann: 29005 # 数据集标注框总数
!python tools/box_distribution.py --json_path dataset/Vehicle/voc_train.json --out_img box_distribution.jpg --eval_size 640 --small_stride 8
五、代码实现
5.1 baseline
在本项目中我选择的PP-YOLOE-SOD系列模型。
PaddleDetection团队提供了针对VisDrone-DET、DOTA水平框、Xview等小目标场景数据集的基于PP-YOLOE改进的检测模型 PP-YOLOE-SOD,以及提供了一套使用SAHI(Slicing Aided Hyper Inference)工具的切图和拼图的方案。
PP-YOLOE-SOD 是PaddleDetection团队自研的小目标检测特色模型,使用数据集分布相关的基于向量的DFL算法 和 针对小目标优化的中心先验优化策略,并且在模型的Neck(FPN)结构中加入Transformer模块,以及结合增加P2层、使用large size等策略,最终在多个小目标数据集上达到极高的精度。
在运行过程中我们可能会遇到以下问题:
Error: /paddle/paddle/phi/kernels/gpu/one_hot_kernel.cu:38 Assertion `p_in_data[idx] >= 0 && p_in_data[idx] < depth` failed. Illegal index value, Input(input) value should be greater than or equal to 0, and less than depth [70644], but received [94393729526616].
解决方案:学习率lr调低10倍,具体可以参考PPYOLOE:又快又好的小目标检测训练与部署实现。
!python tools/train.py -c configs/smalldet/ppyoloe_plus_sod_crn_l_80e_coco.yml --amp --eval --use_vdl True --vdl_log_dir vdl_log_dir/ppyoloe_plus_sod_crn_l_80e_coco
损失函数如图所示:
!python tools/eval.py -c configs/smalldet/ppyoloe_plus_sod_crn_l_80e_coco.yml -o weights=output/ppyoloe_plus_sod_crn_l_80e_coco/best_model.pdparams
模型的各项指标如下所示:
- Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.643
- Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.953
- Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.744
- Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.564
- Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.732
- Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.794
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 1 ] = 0.126
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 10 ] = 0.553
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.706
- Average Recall (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.646
- Average Recall (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.787
- Average Recall (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.822
- Total sample number: 558, average FPS: 17.377526192569604
我们可以对上述指标进行一定的定量分析:
- IoU=0.50意味着IoU大于0.5被认为是检测到,即将IoU设为0.5时,计算每一类的所有图片的AP,然后所有类别求平均,即mAP。
- IoU=0.50:0.95意味着IoU在0.5到0.95的范围内被认为是检测到,表示在不同IoU阈值(从0.50到0.95,步长0.05)(0.5、0.55、0.6、0.65、0.7、0.75、0.8、0.85、0.9、0.95)上的平均mAP。
- 越低的IoU阈值,则判为正确检测的越多,相应的,Average Precision (AP)也就越高。
- small表示标注的框面积小于32 * 32;
- medium表示标注的框面积同时小于96 * 96;
- large表示标注的框面积大于等于96 * 96;
- all表示不论大小,我都要。
- maxDets=100表示最大检测目标数为100。
5.2 涨点方案!大尺度VS切图!
5.2.1 大尺度训练
我们输入模型的尺寸是[1080,1920]和[720,1280],而模型的输入尺寸是[640,640]。将数据输入模型的时候不可避免地会损失一定的细节信息,同时本项目又存在大量的小目标检测物体,因此会导致模型精度的下降。我们很自然地首先就想到可以使用大尺度进行训练。
本项目是基于1600尺度为基础的多尺度训练,将训练的batch_size减小,以速度来换取高精度。
!python tools/train.py -c configs/smalldet/visdrone/ppyoloe_plus_sod_crn_l_largesize_80e_visdrone.yml --amp --eval --use_vdl True --vdl_log_dir vdl_log_dir/ppyoloe_plus_sod_crn_l_largesize_80e_visdrone
损失函数如图所示:
!python tools/eval.py -c configs/smalldet/visdrone/ppyoloe_plus_sod_crn_l_largesize_80e_visdrone.yml -o weights=output/ppyoloe_plus_sod_crn_l_largesize_80e_visdrone/best_model.pdparams
模型的各项指标如下所示:
- Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.633
- Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.973
- Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.734
- Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.568
- Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.698
- Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.743
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 1 ] = 0.120
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 10 ] = 0.553
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.706
- Average Recall (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.658
- Average Recall (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.766
- Average Recall (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.770
- Total sample number: 558, average FPS: 4.898954086318949
我们可以看到,大尺度训练在该任务中并未达到很好的涨点效果,精度下降了1%,同时速度也发生了大幅度的下降。感兴趣的小伙伴可以自行调参看是否能够达到更好的效果。
5.2.2 切图训练
针对超大分辨率的数据集,我们还有一种方法就是使用切图后的子图训练。
Step01: 安装SAHI和必要的依赖库。
!pip install sahi
!pip install -U scikit-image imagecodecs
Step02: 基于SAHI切图。
PP-YOLOE-SOD现支持的切图尺寸有三种:
模型 | 数据集 | SLICE_SIZE | OVERLAP_RATIO |
---|---|---|---|
PP-YOLOE-P2-l | DOTA | 500 | 0.25 |
PP-YOLOE-P2-l | Xview | 400 | 0.25 |
PP-YOLOE-l | VisDrone-DET | 640 | 0.25 |
在上文我们提到,本项目数据集的尺寸是[1080,1920]和[720,1280],本身尺寸并没有特别的大,所以我决定以500为基础进行切图。
!python tools/slice_image.py --image_dir dataset/Vehicle/JPEGImages --json_path dataset/Vehicle/voc_train.json --output_dir dataset/Vehicle_sliced --slice_size 500 --overlap_ratio 0.25
!python tools/slice_image.py --image_dir dataset/Vehicle/JPEGImages --json_path dataset/Vehicle/voc_val.json --output_dir dataset/Vehicle_sliced --slice_size 500 --overlap_ratio 0.25
Step03: 训练。
在训练过程中我们可能会遇到如下问题:
ValueError: (InvalidArgument) The 2-th dimension of input[0] and input[1] is expected to be equal.But received input[0]'s shape = [1, 192, 64, 64], input[1]'s shape = [1, 256, 63, 63].
[Hint: Expected inputs_dims[0][j] == inputs_dims[i][j], but received inputs_dims[0][j]:64 != inputs_dims[i][j]:63.] (at /paddle/paddle/phi/kernels/funcs/concat_funcs.h:83)
解决方案:需要我们注意的是,虽然我们切图后的子图尺寸是500,但500不是32的倍数,不能直接输入模型进行训练,所以我们训练的尺寸可以设置为512。
同时切图后可能会存在大量的负样本,但这不影响我们后续的训练。
ppdet.data.source.coco INFO: Load [17215 samples valid, 16259 samples invalid] in file dataset/Vehicle_sliced/voc_train_500_025.json.
ppdet.data.source.coco INFO: Load [3511 samples valid, 3319 samples invalid] in file dataset/Vehicle_sliced/voc_val_500_025.json.
!python tools/train.py -c configs/smalldet/ppyoloe_p2_crn_l_80e_sliced_DOTA_500_025.yml --amp --eval --use_vdl True --vdl_log_dir vdl_log_dir/ppyoloe_p2_crn_l_80e_sliced_DOTA_500_025
损失函数如图所示:
!python tools/eval.py -c configs/smalldet/ppyoloe_p2_crn_l_80e_sliced_DOTA_500_025.yml -o weights=output/ppyoloe_p2_crn_l_80e_sliced_DOTA_500_025/best_model.pdparams
- Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.710
- Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.980
- Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.830
- Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.644
- Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.776
- Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.796
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 1 ] = 0.309
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 10 ] = 0.710
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.764
- Average Recall (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.712
- Average Recall (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.827
- Average Recall (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.827
- Total sample number: 3511, average FPS: 23.05708607036652
在切分的子图上,模型约能涨点6.7%,速度提升5.68。
!python tools/eval.py -c configs/smalldet/ppyoloe_p2_crn_l_80e_sliced_DOTA_500_025_infer.yml -o weights=output/ppyoloe_p2_crn_l_80e_sliced_DOTA_500_025/best_model.pdparams --slice_infer --combine_method=nms --match_threshold=0.6 --match_metric=ios
评估模型的时候,我们可以选择子图拼图评估,这样更接近实际的检测效果。
- Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.639
- Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.955
- Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.737
- Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.615
- Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.681
- Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.627
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 1 ] = 0.124
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 10 ] = 0.568
- Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.716
- Average Recall (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.690
- Average Recall (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.751
- Average Recall (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.675
- Total sample number: 558, average FPS: 0.6696922658566277
我们可以看到,切图训练精度损失约0.2%,但不可避免地推理速度发生了大幅度的下降。
Step03: 预测。
我们可以预测部分图片,观察一下模型的实际检测效果。
!python tools/infer.py -c configs/smalldet/ppyoloe_p2_crn_l_80e_sliced_DOTA_500_025.yml -o weights=output/ppyoloe_p2_crn_l_80e_sliced_DOTA_500_025/best_model.pdparams --infer_img=dataset/Vehicle/JPEGImages/sec7_frame_000943.png --draw_threshold=0.5 --slice_infer --slice_size 500 500 --overlap_ratio 0.25 0.25 --combine_method=nms --match_threshold=0.6 --match_metric=ios --save_results=True
部分可视化结果如下:
5.3 模型导出
接下来我将以ppyoloe_plus_sod_crn_l_80e_coco模型为例介绍OpenVINO-Python部署方案,因此首先需要先导出相应的模型。
!python tools/export_model.py -c configs/smalldet/ppyoloe_plus_sod_crn_l_80e_coco.yml -o weights=output/ppyoloe_plus_sod_crn_l_80e_coco/best_model.pdparams
5.4 模型压缩
首先我们需要安装PaddleSlim。
!pip install paddleslim
本项目选择的是在线量化的方式。一般量化训练直接在训好的浮点模型上进行finetune少量Epoch即可,finetune过程中,学习率也需要适当调小。量化训练的优点是在训练中调整权重分布以适应模拟量化计算,从而大幅降低量化模型的精度损失,一般优于离线量化方法。缺点是训练过程较慢,资源要求较高。
!python tools/train.py -c configs/smalldet/ppyoloe_plus_sod_crn_l_80e_coco.yml --slim_config configs/slim/quant/ppyoloe_plus_sod_crn_l_80e_coco_qat.yml -r output/ppyoloe_plus_sod_crn_l_80e_coco_qat/14.pdparams
运行以下代码块导出训练好的量化模型。
!python tools/export_model.py -c configs/smalldet/ppyoloe_plus_sod_crn_l_80e_coco.yml --slim_config configs/slim/quant/ppyoloe_plus_sod_crn_l_80e_coco_qat.yml -o weights=output/ppyoloe_plus_sod_crn_l_80e_coco_qat/model_final
计算模型推理时延前需要安装部分依赖。
!pip install GPUtil --user
由于统计整个测试集图片的时间过长,我决定取出一百张图片进行测试。
import os
import cv2
img_name = []
def create_test_dir(img_dir):
count = 0
for filename in os.listdir(img_dir):
if count==100:
break
count = count + 1
img = cv2.imread(img_dir + "/" + filename)
cv2.imwrite("/home/aistudio/PaddleDetection/dataset/Vehicle/benchmark/"+filename, img)
img_name.append(img)
print(filename)
create_test_dir("/home/aistudio/PaddleDetection/dataset/Vehicle/JPEGImages")
!python deploy/python/infer.py --model_dir=output_inference/ppyoloe_plus_sod_crn_l_80e_coco --image_dir=dataset/Vehicle/benchmark --device=GPU --run_benchmark True --run_mode paddle
- cpu_rss(MB): 3162, cpu_vms: 0, cpu_shared_mb: 0, cpu_dirty_mb: 0, cpu_util: 0%
- gpu_rss(MB): 1641, gpu_util: 0.14%, gpu_mem_util: 0%
- total time spent(s): 8.6659
- preprocess_time(ms): 45.5, inference_time(ms): 41.2, postprocess_time(ms): 0.0
!python deploy/python/infer.py --model_dir=output_inference/ppyoloe_plus_sod_crn_l_80e_coco_qat --image_dir=dataset/Vehicle/benchmark --device=GPU --run_benchmark True --run_mode trt_int8 --run_mode False
- cpu_rss(MB): 3461, cpu_vms: 0, cpu_shared_mb: 0, cpu_dirty_mb: 0, cpu_util: 0%
- gpu_rss(MB): 1647, gpu_util: 0.12%, gpu_mem_util: 0%
- total time spent(s): 8.0766
- preprocess_time(ms): 47.9, inference_time(ms): 32.8, postprocess_time(ms): 0.0
可以看到,在线量化后模型的推理速度提升了20%。
六、OpenVINO-Python部署方案
OpenVINO runtime api的调用流程如图所示:
本项目现已基于此流程将模型在本地部署实现。
项目部署环境:
- 笔记本型号:R9000P
- 操作系统:Windows10
- Cuda版本:11.6
- OpenVINO版本:2022.3
- 编译器环境:Pycharm
- Create OpenVINO Runtime Core:
ie_core = Core()
- Compile Model:
det_model = ie_core.read_model(model="ppyoloe_plus_sod_crn_l_80e_coco/model.pdmodel")
det_model.reshape({'image': [1, 3, 640, 640], 'scale_factor': [1, 2]})
detector = ie_core.compile_model(model=det_model, device_name="CPU")
3 & 4. Create Inference Request & Set Inputs:
image = cv2.imread("sec3_frame_000700.png")
scale_factor = np.array([[1, 2]]).astype('float32') # 超参数
img = cv2.resize(image, (640, 640)) # 尺寸缩放
img = np.transpose(img, [2, 0, 1]) / 255 # 归一化
img = np.expand_dims(img, 0) # 拓展维度(C, H, W) -> (1, C, H, W)
img_mean = np.array([0.0, 0.0, 0.0]).reshape((3, 1, 1))
img_std = np.array([1.0, 1.0, 1.0]).reshape((3, 1, 1))
img -= img_mean
img /= img_std
inputs_dict = {'image': img, "scale_factor": scale_factor}
- Start Inference:
output_layer = detector.output(0)
results = detector(inputs_dict)[output_layer]
- Process Inference Result:
score_threshold = 0.5 # 输出阈值
filtered_results = []
for i in range(len(results)):
if results[i, 1] > score_threshold:
filtered_results.append(results[i])
det_image = det_visualization(image, filtered_results)
可视化结果如下:
同时我已经将该推理部署文件上传至/home/aistudio/work目录下,大家可以自行查看!
六、总结提高
本项目基于PP-YOLOE-SOD实现了基于无人机影像的小目标车辆检测,并比较了大尺度和切图两种涨点方案,具体结果如下:
模型名称 | IoU=0.50:0.95 | FPS |
---|---|---|
ppyoloe_plus_sod_crn_l_80e_coco | 0.643 | 17.378 |
ppyoloe_plus_sod_crn_l_largesize_80e_visdrone | 0.633 | 4.899 |
ppyoloe_plus_sod_crn_l_80e_coco | 0.710 | 23.057 |
切图后模型在子图上验证约有6.7%的精度提升。
后续工作:
- 后期会在OpenVINO上实现C++环境下的模型部署。
- 进一步压缩模型达到更好的落地效果。
此文章为搬运
原项目链接
更多推荐
所有评论(0)