detr容器部署和训练VOC数据集

获取源码

开源地址 git clone https://github.com/facebookresearch/detr.git

训练环境部署

这里使用的资源是在内网服务器上,首先当然要预装好可以挂载gpu的docker,Nvidia的显卡等相关驱动,输入nvidia-smi 可以看到GPU就可以了。
然后打包可以运行detr的容器镜像:编辑Dockerfile文件

FROM pytorch/pytorch:1.5-cuda10.1-cudnn7-runtime

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update -qq && \
    apt-get install -y git vim libgtk2.0-dev && \
    rm -rf /var/cache/apk/*

RUN pip --no-cache-dir install Cython

RUN git clone https://github.com/philferriere/cocoapi.git

RUN cd cocoapi && cd PythonAPI \
    make

RUN pip --no-cache-dir install pycocotools

RUN cd .. && git clone https://gitee.com/qiaodl/panopticapi.git && \
    cd panopticapi && \
    python setup.py install


# 强烈建议修改
COPY requirements.txt /workspace

RUN pip install  --no-cache-dir -r  /workspace/requirements.txt

修改python依赖包, cat requirements.txt

submitit
torch>=1.5.0
torchvision
scipy
onnx
onnxruntime

构建镜像 docker build -t yihui8776/detr:v0.1 .
镜像可以从docker hub上pull
docker pull yihui8776/detr:v0.1

数据准备

voc和coco数据集

我们可能更多人知道著名的ImageNet,ImageNet是斯坦福大学李飞飞教授主持设立的关于计算机视觉的数据库,有上千万图片,数万个分类,其实就是模拟人的认知系统设立的视觉项目,也是深度学习领域一个非常火热的竞赛。

同时视觉领域还有很多比较实用的数据集,相对来说我们平时可以更容易使用和训练
,如VOC的数据集 ,有20分类,Coco数据集有91分类。

VOC 数据集是最为常用的数据集,而且VOC的数据格式也是比较直观通用,我们如果训练自己的数据也是要先使用标注软件 类似labelImg,进行打标生成图片相应的xml文件,也就是VOC主要保存格式。

数据集网页
voc全名是# The PASCAL Visual Object Classes ,主要是2005到2012年举办的著名图像识别类的比赛,主要包括图像分类,目标检测,图像分割(专业人士可以略过)

  • voc首页 :http://host.robots.ox.ac.uk/pascal/VOC/
  • 查看各位大牛算法的排名的Leaderboards:* http://host.robots.ox.ac.uk:8080/leaderboard/main_bootstrap.php
  • 训练/验证数据集下载(2G):host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar

但是到了2012年项目主要无偿贡献者Mark Everingham 去世了,后面就没有维护,现在可以在 :https://pjreddie.com/projects/pascal-voc-dataset-mirror/ 下载数据集

image.png

最新的数据2012,是包括了2007和2012的所有数据。共17125张图片。

数据集结构
下载voc数据集后查看文件夹结构:

image.png

├── Annotations # 里面存放 .xml 文件,图片的标签,比如坐标位置信息等。
├── ImageSets # 这个目录下有三个文件夹,文件夹存放的都是 .txt 文件,类别标签,
│ ├── Layout
│ ├── Main # Main 目录主要是分train/val 的txt文件,如train.txt就是所有训练集的图片名组成,也含有各个类的训练集txt,包括正负样本,方便各类选择训练,也便于抽样, 这里主要任务是目标检测,所以只要使用这个文件夹。
│ └── Segmentation
├── JPEGImages # 图像文件 .jpg 格式
├── labels
├── SegmentationClass # 存放的是图片文件,分割后的图片
└── SegmentationObject # 存放的是图片文件,分割后的图片

可以从Main中知道有哪些类


[图片上传中...(image.png-1cd64-1626770867618-0)]

可以知道每张图都是有多个类的多个样本,里面有 xx_train.txt, xx_test.txt , xx_trainval.txt, xx_val.txt 文件,xx表示分类,总共20类
人:人

动物:鸟、猫、牛、狗、马、羊

车辆:飞机、自行车、船、巴士、汽车、摩托车、火车

室内:瓶、椅子、餐桌、盆栽植物、沙发、电视/监视器

所以这里做目标检测主要保留这些文件夹
├── Annotations
├── ImageSets
├── ├── Main
├── JPEGImages

Annotation

Annotations文件夹中存放的是xml格式的标签文件,每一个xml文件都对应于JPEGImages文件夹中的一张图片。


image.png
03414743cfc5605888c3ccdec7f095c9_SouthEast.png

JPEGImages文件夹中包含了PASCAL VOC所提供的所有的图片,包含训练图片和测试图片,共有17125张。图片均以“年份_编号.jpg”格式命名。图片的尺寸大小不一,所以在后面训练的时候需要对图片进行resize操作。

图片的像素尺寸大小不一,但是横向图的尺寸大约在500 * 375左右,纵向图的尺寸大约在375 * 500左右,基本不会偏差超过100。(在之后的训练中,第一步就是将这些图片都resize到300*300或是500 * 500,所有原始图片不能离这个标准过远)

更多查看 https://blog.csdn.net/xingwei_09/article/details/79142558
https://pjreddie.com/media/files/VOC2012_doc.pdf

coco数据集
coco数据集叫Microsoft COCO(Common Objects in Context)
类别更多有91个,图片超过300000个,而且每张图片的类别也更多,难度更大

  • MS COCO 数据集主页:http://mscoco.org/
  • Github 网址:https://github.com/Xinering/cocoapi
  • 关于 API 更多的细节在网站: http://mscoco.org/dataset/#download

1、2014年数据集的下载
http://msvocds.blob.core.windows.net/coco2014/train2014.zip

2、2017的数据集的下载
http://images.cocodataset.org/zips/train2017.zip
http://images.cocodataset.org/annotations/annotations_trainval2017.zip

http://images.cocodataset.org/zips/val2017.zip
http://images.cocodataset.org/annotations/stuff_annotations_trainval2017.zip

http://images.cocodataset.org/zips/test2017.zip
http://images.cocodataset.org/annotations/image_info_test2017.zip

coco api
coco数据集可以直接使用api进行操作,方便调用,如sklearn的dataset一样,同时coco API也有python matlab lua等不同版本,在部署阶段我们也看到下载,detr的代码也是直接调用coco API的

coco数据集下载解压后,文件夹主要就是标注文件 annotation和图片集,一般分为train和val两个文件夹保存;
path/to/coco/
├── annotations/ # 标注json文件
├── train2017/ # 训练集图片
├── val2017/ # 验证集图片

COCO数据集现在有3种标注类型:object instances(目标实例), object keypoints(目标上的关键点), 和image captions(看图说话),使用JSON文件存储。比如 2017的如下


image.png

详细信息

image.png

参考
https://blog.csdn.net/wc781708249/article/details/79603522
https://blog.csdn.net/bestrivern/article/details/88846977

voc格式转coco格式

和detr一样,detr 家族的项目很多是针对coco数据集的,所以我们自己要转换为coco格式。
这个很多案例和开源,这里只是做个记录和整合,用已有的轮子改

先将数据集划分,可以直接用voc 的main的txt ,生成,也可以对图片数据直接打乱抽样,做自己的训练和验证集合。

直接对图片进行抽取,按比例复制到各文件夹

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# 将一个文件夹下图片按比例分在三个文件夹下
import os
import random
import shutil
from shutil import copy2

datadir_normal = "./VOCdevkit/VOC2019/JPEGImages/"

all_data = os.listdir(datadir_normal)  # (图片文件夹)
num_all_data = len(all_data)
print("num_all_data: " + str(num_all_data))
index_list = list(range(num_all_data))
# print(index_list)
random.shuffle(index_list)
num = 0

trainDir = "./cocodata/train/"  # (将训练集放在这个文件夹下)
if not os.path.exists(trainDir):
    os.mkdir(trainDir)

validDir = './cocodata/val/'  # (将验证集放在这个文件夹下)
if not os.path.exists(validDir):
    os.mkdir(validDir)

testDir = './cocodata/test/'  # (将测试集放在这个文件夹下)
if not os.path.exists(testDir):
    os.mkdir(testDir)

for i in index_list:
    fileName = os.path.join(datadir_normal, all_data[i])
    if num < num_all_data * 0.6:
        # print(str(fileName))
        copy2(fileName, trainDir)
    elif num > num_all_data * 0.6 and num < num_all_data * 0.8:
        # print(str(fileName))
        copy2(fileName, validDir)
    else:
        copy2(fileName, testDir)
    num += 1

当然最好的是直接先将 train和val的图片id 保存为txt格式文件,就如Main目录下的,然后在进行图片和标注文件的复制转移。最后将xml转为json格式

import os
import random

trainval_percent = 0.9
train_percent = 0.8
xmlfilepath = 'Annotations'
txtsavepath = 'ImageSets\Main'
total_xml = os.listdir(xmlfilepath)

num=len(total_xml)
list=range(num)
tv=int(num*trainval_percent)
tr=int(tv*train_percent)
trainval= random.sample(list,tv)
train=random.sample(trainval,tr)

ftrainval = open('ImageSets/Main/trainval1.txt', 'w')
ftest = open('ImageSets/Main/test1.txt', 'w')
ftrain = open('ImageSets/Main/train1.txt', 'w')
fval = open('ImageSets/Main/val1.txt', 'w')

for i  in list:
    name=total_xml[i][:-4]+'\n'
    if i in trainval:
        ftrainval.write(name)
        if i in train:
            ftrain.write(name)
        else:
            fval.write(name)
    else:
        ftest.write(name)

ftrainval.close()
ftrain.close()
fval.close()
ftest .close()

根据txt文件生成相应图片文件夹


import os, random, shutil
from shutil import copy2
   
if __name__ == '__main__':
    fileDir = "E:/yolo3/VOCdevkit/VOC2012/JPEGImages/"    #源图片文件夹路径
    trainDir = 'E:/yolo3/VOCdevkit/VOC2012/train2017/'    #移动到新的文件夹路径
    valDir = 'E:/yolo3/VOCdevkit/VOC2012/val2017/'
    testDir = 'E:/yolo3/VOCdevkit/VOC2012/test2017/'


    train = []
    with open('E:/yolo3/VOCdevkit/VOC2012/ImageSets/Main/train1.txt', 'r') as f:
        for line in f:
            train.append(line.strip('\n'))
    #print(train)
    for  name in train:
        shutil.copy2(fileDir + name+'.jpg', trainDir + name+'.jpg')

    val = []
    with open('E:/yolo3/VOCdevkit/VOC2012/ImageSets/Main/val1.txt', 'r') as f:
        for line in f:
            val.append(line.strip('\n'))
        # print(train)
    for name in val:
        shutil.copy2(fileDir + name + '.jpg', valDir + name + '.jpg')

    test = []
    with open('E:/yolo3/VOCdevkit/VOC2012/ImageSets/Main/test1.txt', 'r') as f:
        for line in f:
            test.append(line.strip('\n'))
            # print(train)
    for name in test:
        shutil.copy2(fileDir + name + '.jpg', testDir + name + '.jpg')

同样可以移动xml 标注文件

import os, random, shutil
 
if __name__ == '__main__':
    fileDir = "E:/yolo3/VOCdevkit/VOC2012/Annotations/"    #源图片文件夹路径
    trainDir = 'E:/yolo3/VOCdevkit/VOC2012/xml/xml_train/'    #移动到新的文件夹路径
    valDir = 'E:/yolo3/VOCdevkit/VOC2012/xml/xml_val/'
    testDir = 'E:/yolo3/VOCdevkit/VOC2012/xml/xml_test/'


    train = []
    with open('E:/yolo3/VOCdevkit/VOC2012/ImageSets/Main/train1.txt', 'r') as f:
        for line in f:
            train.append(line.strip('\n'))
    #print(train)
    for  name in train:
        shutil.copy2(fileDir + name+'.xml', trainDir + name+'.xml')

    val = []
    with open('E:/yolo3/VOCdevkit/VOC2012/ImageSets/Main/val1.txt', 'r') as f:
        for line in f:
            val.append(line.strip('\n'))
        # print(train)
    for name in val:
        shutil.copy2(fileDir + name + '.xml', valDir + name + '.xml')

    test = []
    with open('E:/yolo3/VOCdevkit/VOC2012/ImageSets/Main/test1.txt', 'r') as f:
        for line in f:
            test.append(line.strip('\n'))
            # print(train)

    for name in test:
        shutil.copy2(fileDir + name + '.xml', testDir + name + '.xml')

转换xml 为json, voc2coco.py

#!/usr/bin/python

# pip install lxml

import sys
import os
import json
import xml.etree.ElementTree as ET
import glob

START_BOUNDING_BOX_ID = 1
PRE_DEFINE_CATEGORIES = None
# If necessary, pre-define category and its id
#  PRE_DEFINE_CATEGORIES = {"aeroplane": 1, "bicycle": 2, "bird": 3, "boat": 4,
#  "bottle":5, "bus": 6, "car": 7, "cat": 8, "chair": 9,
#  "cow": 10, "diningtable": 11, "dog": 12, "horse": 13,
#  "motorbike": 14, "person": 15, "pottedplant": 16,
#  "sheep": 17, "sofa": 18, "train": 19, "tvmonitor": 20}


def get(root, name):
    vars = root.findall(name)
    return vars


def get_and_check(root, name, length):
    vars = root.findall(name)
    if len(vars) == 0:
        raise ValueError("Can not find %s in %s." % (name, root.tag))
    if length > 0 and len(vars) != length:
        raise ValueError(
            "The size of %s is supposed to be %d, but is %d."
            % (name, length, len(vars))
        )
    if length == 1:
        vars = vars[0]
    return vars

#
# def get_filename_as_int(filename):
#     try:
#         filename = filename.replace("\\", "/")
#         filename = os.path.splitext(os.path.basename(filename))[0]
#         return int(filename)
#     except:
#         raise ValueError("Filename %s is supposed to be an integer." % (filename))



def get_filename_as_integer(filename):
    filename = filename.replace("\\", "/")
    filename = os.path.splitext(os.path.basename(filename))[0]
    filename1 =filename.split('_')
    filename2 = ''
    for i in range(len(filename1)):
        filename2 += filename1[i]
    return int(filename2)



def get_categories(xml_files):
    """Generate category name to id mapping from a list of xml files.
    
    Arguments:
        xml_files {list} -- A list of xml file paths.
    
    Returns:
        dict -- category name to id mapping.
    """
    classes_names = []
    for xml_file in xml_files:
        tree = ET.parse(xml_file)
        root = tree.getroot()
        for member in root.findall("object"):
            classes_names.append(member[0].text)
    classes_names = list(set(classes_names))
    classes_names.sort()
    return {name: i for i, name in enumerate(classes_names)}


def convert(xml_files, json_file):
    json_dict = {"images": [], "type": "instances", "annotations": [], "categories": []}
    if PRE_DEFINE_CATEGORIES is not None:
        categories = PRE_DEFINE_CATEGORIES
    else:
        categories = get_categories(xml_files)
    bnd_id = START_BOUNDING_BOX_ID
    for xml_file in xml_files:
        tree = ET.parse(xml_file)
        root = tree.getroot()
        path = get(root, "path")
        if len(path) == 1:
            filename = os.path.basename(path[0].text)
        elif len(path) == 0:
            filename = get_and_check(root, "filename", 1).text
        else:
            raise ValueError("%d paths found in %s" % (len(path), xml_file))
        ## The filename must be a number
        #image_id = get_filename_as_int(filename)
        image_id = get_filename_as_integer(filename)
        size = get_and_check(root, "size", 1)
        width = int(get_and_check(size, "width", 1).text)
        height = int(get_and_check(size, "height", 1).text)
        image = {
            "file_name": filename,
            "height": height,
            "width": width,
            "id": image_id,
        }
        json_dict["images"].append(image)
        ## Currently we do not support segmentation.
        #  segmented = get_and_check(root, 'segmented', 1).text
        #  assert segmented == '0'
        for obj in get(root, "object"):
            category = get_and_check(obj, "name", 1).text
            if category not in categories:
                new_id = len(categories)
                categories[category] = new_id
            category_id = categories[category]
            bndbox = get_and_check(obj, "bndbox", 1)
            xmin = int(get_and_check(bndbox, "xmin", 1).text) - 1
            ymin = int(float((get_and_check(bndbox, "ymin", 1).text)))- 1
            xmax = int(get_and_check(bndbox, "xmax", 1).text)
            ymax = int(get_and_check(bndbox, "ymax", 1).text)
            assert xmax > xmin
            assert ymax > ymin
            o_width = abs(xmax - xmin)
            o_height = abs(ymax - ymin)
            ann = {
                "area": o_width * o_height,
                "iscrowd": 0,
                "image_id": image_id,
                "bbox": [xmin, ymin, o_width, o_height],
                "category_id": category_id,
                "id": bnd_id,
                "ignore": 0,
                "segmentation": [],
            }
            json_dict["annotations"].append(ann)
            bnd_id = bnd_id + 1

    for cate, cid in categories.items():
        cat = {"supercategory": "none", "id": cid, "name": cate}
        json_dict["categories"].append(cat)

    os.makedirs(os.path.dirname(json_file), exist_ok=True)
    json_fp = open(json_file, "w")
    json_str = json.dumps(json_dict)
    json_fp.write(json_str)
    json_fp.close()


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        description="Convert Pascal VOC annotation to COCO format."
    )
    parser.add_argument("xml_dir", help="Directory path to xml files.", type=str)
    parser.add_argument("json_file", help="Output COCO format json file.", type=str)
    args = parser.parse_args()
    xml_files = glob.glob(os.path.join(args.xml_dir, "*.xml"))

    # If you want to do train/test split, you can pass a subset of xml files to convert function.
    print("Number of xml files: {}".format(len(xml_files)))
    convert(xml_files, args.json_file)
    print("Success: {}".format(args.json_file))

运行只要执行 python voc2coco sourcedir targetdir 如
python voc2coco.py ../xml/xml_train ./data/coco/instance_train2017.json

当然这些步骤可以集成到一个文件一键操作。
参考: https://github.com/Tony607/voc2coco

目前VOC比较常用的是2007和2012数据集,但是2012没有test,所以多使用这两个组合, 可以使用 VOC2007 和 VOC2012 的 train+val(16551) 上训练,然后使用 VOC2007 的 test(4952) 测试。
当然还要别的组合 可以参考:
https://blog.csdn.net/mzpmzk/article/details/88065416

配置更改

数据准备好了,接下来就要根据自己的数据进行代码的修改
我们当然可以从头训练自己的深度学习模型,但是比较难的,而且需要很多资源,所以一般使用预训练模型,也就是已经在别的数据集上训练过的模型拿来再进行训练。
如图像的通常指的是在Imagenet上训练的CNN

这里我们可以直接下载 detr 训练coco数据集的模型,也就是91类的模型架构,
下载模型 :
wget https://dl.fbaipublicfiles.com/detr/detr-r50-e632da11.pth
这是detr 训练的 resnet50 经典的cnn结构
这些模型也可通过torch hub找到,以用预训练的权重加载DETR R50,只需执行以下操作:
model = torch.hub.load('facebookresearch/detr', 'detr_resnet50', pretrained=True)

PyTorch Hub是一个简易API和工作流程,为复现研究提供了基本构建模块,包含预训练模型库。并且,PyTorch Hub还支持Colab,能与论文代码结合网站Papers With Code集成,用于更广泛的研究。torch hub使得我们可以更好地推广和获取各种训练模型,相当于模型管理的仓库和API。可以集成到python代码里。
如查询可用模型:

torch.hub.list('pytorch/vision')

['alexnet',
'deeplabv3_resnet101',
'densenet121',
...
'vgg16',
'vgg16_bn',
'vgg19',
 'vgg19_bn']

detr相关的hub配置全放在 hubconf.py文件里。


image.png

更改模型结构 ,适应自己的类别个数: change.py

pretrained_weights = torch.load("./detr-r50-e632da11.pth")
 
num_class = 20 + 1 # 类别+1
pretrained_weights["model"]["class_embed.weight"].resize_(num_class+1,256)
pretrained_weights["model"]["class_embed.bias"].resize_(num_class+1)
torch.save(pretrained_weights,'detr_r50_%d.pth'%num_class)

这样生成新的预训练模型

修改模型代码部分 models/detr.py


image.png

这里也说明到,要将参数设为类别+1,一个是背景类

所有准备工作基本都好了,可以开始炼丹了

这里使用的是docker ,要注意因为内存使用的比较大,而docker 有限制共享内存 默认是64M ,所有运行docker 时候要加上参数 --shm-size

将数据集挂载到代码目录的data下,

docker run --gpus all -itd --shm-size 8G -v /media/nizhengqi/7a646073-10bf-41e4-93b5-4b89df793ff8/wyh/data:/workspace/data -v /media/nizhengqi/7a646073-10bf-41e4-93b5-4b89df793ff8/wyh/detr:/workspace --name detr1 yihui8776/detr:v0.1

进入docker 容器
docker exec -it detr1 /bin/bash
开始训练
python main.py --coco_path "data" --epoch 1000 --batch_size=2 --num_workers=4 --output_dir="outputs_1" --resume="detr_r50_21.pth"

image.png

第一阶段完成,接下来就是fine-tune了。

测评
这里就训练 了62 个epochs
选用这时的checkpoint进行评价,还是用trainval数据集,当然最好用没用过的测试集
python main.py --batch_size 1 --no_aux_loss --eval --resume ./outputs_1/checkpoint.pth --coco_path data


image.png

python main.py --batch_size 2 --no_aux_loss --eval --resume ./outputs_1/checkpoint.pth --coco_path data


image.png
image.png

参考:
https://www.jianshu.com/p/d7a06a720a2b

https://github.com/DataXujing/detr_transformer
https://www.bilibili.com/video/BV1GC4y1h77h

你可能感兴趣的:(detr容器部署和训练VOC数据集)