★★★ 本文源自AlStudio社区精品项目,【点击此处】查看更多精品内容 >>>
本项目基于PP-YOLOE-SOD实现了基于无人机影像的小目标车辆检测,并比较了大尺度和切图两种涨点方案,具体结果如下:

模型名称IoU=0.50:0.95FPS
ppyoloe_plus_sod_crn_l_80e_coco0.64317.378
ppyoloe_plus_sod_crn_l_largesize_80e_visdrone0.6334.899
ppyoloe_plus_sod_crn_l_80e_coco0.71023.057

切图后模型在子图上验证约有6.7%的精度提升,并提供了一种模型量化的方案。

一、项目背景

无人机的使用始于21世纪初的军事目的,但随着时间的推移,它们已开始用于其他领域。交通管理中最具挑战性的问题之一是复杂的场景,例如在交通环形路口和交叉路口。在这些复杂的场景中,无人机可用于与自动驾驶汽车或交通基础设施相结合,为它们提供没有这种空中视野就无法获得的信息。

智能交通领域的其他工作包括RGB热图像的语义分割以及突出物体的识别。在这些任务中,传统的RGB图像与热图像以及深度图像相结合,以利用所有可用的环境信息。

二、数据集介绍

这是一个从无人机 (UAV) 获取的西班牙道路交通图像数据集,旨在用于训练深度学习模型以提高道路交通视觉和管理系统的性能。

数据集由 15070 张 png 格式的图像组成,并附有对应txt格式的标注文件。这些图像在城市道路的六个不同位置拍摄,标记有155328辆汽车,包括汽车(137602辆)和摩托车(17726辆)。

ScenesSequencesFramesTargetsCarsMotorbikes
Regional roadsec14,50024,85814,57710,281
Urban intersectionsec9, seca, secb, secc2,46210,75910,7590
Rural roadsec71,2927467460
Split roundaboutsec83,1073,1073,1070
Roundabout (far)sec2, sec31,81471,81964,8446,975
Roundabout (near)sec4, sec5, sec63,99744,03943,569470
Total15,070155,328137,60217,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_SIZEOVERLAP_RATIO
PP-YOLOE-P2-lDOTA5000.25
PP-YOLOE-P2-lXview4000.25
PP-YOLOE-lVisDrone-DET6400.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
  1. Create OpenVINO Runtime Core:
ie_core = Core()
  1. 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}
  1. Start Inference:
output_layer = detector.output(0)
results = detector(inputs_dict)[output_layer]
  1. 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.95FPS
ppyoloe_plus_sod_crn_l_80e_coco0.64317.378
ppyoloe_plus_sod_crn_l_largesize_80e_visdrone0.6334.899
ppyoloe_plus_sod_crn_l_80e_coco0.71023.057

切图后模型在子图上验证约有6.7%的精度提升。

后续工作:

  • 后期会在OpenVINO上实现C++环境下的模型部署。
  • 进一步压缩模型达到更好的落地效果。

此文章为搬运
原项目链接

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐