获取源码
开源地址 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/ 下载数据集
最新的数据2012,是包括了2007和2012的所有数据。共17125张图片。
数据集结构
下载voc数据集后查看文件夹结构:
├── Annotations # 里面存放 .xml 文件,图片的标签,比如坐标位置信息等。
├── ImageSets # 这个目录下有三个文件夹,文件夹存放的都是 .txt 文件,类别标签,
│ ├── Layout
│ ├── Main # Main 目录主要是分train/val 的txt文件,如train.txt就是所有训练集的图片名组成,也含有各个类的训练集txt,包括正负样本,方便各类选择训练,也便于抽样, 这里主要任务是目标检测,所以只要使用这个文件夹。
│ └── Segmentation
├── JPEGImages # 图像文件 .jpg 格式
├── labels
├── SegmentationClass # 存放的是图片文件,分割后的图片
└── SegmentationObject # 存放的是图片文件,分割后的图片
可以从Main中知道有哪些类
可以知道每张图都是有多个类的多个样本,里面有 xx_train.txt, xx_test.txt , xx_trainval.txt, xx_val.txt 文件,xx表示分类,总共20类
人:人
动物:鸟、猫、牛、狗、马、羊
车辆:飞机、自行车、船、巴士、汽车、摩托车、火车
室内:瓶、椅子、餐桌、盆栽植物、沙发、电视/监视器
所以这里做目标检测主要保留这些文件夹
├── Annotations
├── ImageSets
├── ├── Main
├── JPEGImages
Annotation
Annotations文件夹中存放的是xml格式的标签文件,每一个xml文件都对应于JPEGImages文件夹中的一张图片。
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的如下
详细信息
参考
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文件里。
更改模型结构 ,适应自己的类别个数: 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
这里也说明到,要将参数设为类别+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"
第一阶段完成,接下来就是fine-tune了。
测评
这里就训练 了62 个epochs
选用这时的checkpoint进行评价,还是用trainval数据集,当然最好用没用过的测试集
python main.py --batch_size 1 --no_aux_loss --eval --resume ./outputs_1/checkpoint.pth --coco_path data
python main.py --batch_size 2 --no_aux_loss --eval --resume ./outputs_1/checkpoint.pth --coco_path data
参考:
https://www.jianshu.com/p/d7a06a720a2b
https://github.com/DataXujing/detr_transformer
https://www.bilibili.com/video/BV1GC4y1h77h