獲取源碼
開源地址 git clone https://github.com/facebookresearch/detr.git
訓練環(huán)境部署
這里使用的資源是在內(nèi)網(wǎng)服務器上棠耕,首先當然要預裝好可以掛載gpu的docker炊林,Nvidia的顯卡等相關驅(qū)動,輸入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
構(gòu)建鏡像 docker build -t yihui8776/detr:v0.1 .
鏡像可以從docker hub上pull
docker pull yihui8776/detr:v0.1
數(shù)據(jù)準備
voc和coco數(shù)據(jù)集
我們可能更多人知道著名的ImageNet辉懒,ImageNet是斯坦福大學李飛飛教授主持設立的關于計算機視覺的數(shù)據(jù)庫阳惹,有上千萬圖片,數(shù)萬個分類眶俩,其實就是模擬人的認知系統(tǒng)設立的視覺項目莹汤,也是深度學習領域一個非常火熱的競賽颠印。
同時視覺領域還有很多比較實用的數(shù)據(jù)集纲岭,相對來說我們平時可以更容易使用和訓練
,如VOC的數(shù)據(jù)集 线罕,有20分類止潮,Coco數(shù)據(jù)集有91分類。
VOC 數(shù)據(jù)集是最為常用的數(shù)據(jù)集钞楼,而且VOC的數(shù)據(jù)格式也是比較直觀通用喇闸,我們?nèi)绻柧氉约旱臄?shù)據(jù)也是要先使用標注軟件 類似labelImg,進行打標生成圖片相應的xml文件,也就是VOC主要保存格式燃乍。
數(shù)據(jù)集網(wǎng)頁
voc全名是# The PASCAL Visual Object Classes 唆樊,主要是2005到2012年舉辦的著名圖像識別類的比賽,主要包括圖像分類橘沥,目標檢測窗轩,圖像分割(專業(yè)人士可以略過)
- voc首頁 :http://host.robots.ox.ac.uk/pascal/VOC/
- 查看各位大牛算法的排名的Leaderboards:* http://host.robots.ox.ac.uk:8080/leaderboard/main_bootstrap.php
- 訓練/驗證數(shù)據(jù)集下載(2G):host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar
但是到了2012年項目主要無償貢獻者Mark Everingham 去世了,后面就沒有維護座咆,現(xiàn)在可以在 :https://pjreddie.com/projects/pascal-voc-dataset-mirror/ 下載數(shù)據(jù)集
最新的數(shù)據(jù)2012痢艺,是包括了2007和2012的所有數(shù)據(jù)。共17125張圖片介陶。
數(shù)據(jù)集結(jié)構(gòu)
下載voc數(shù)據(jù)集后查看文件夾結(jié)構(gòu):
├── 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類
人:人
動物:鳥、貓办素、牛魏保、狗、馬摸屠、羊
車輛:飛機、自行車粱哼、船季二、巴士、汽車、摩托車胯舷、火車
室內(nèi):瓶刻蚯、椅子、餐桌桑嘶、盆栽植物炊汹、沙發(fā)、電視/監(jiān)視器
所以這里做目標檢測主要保留這些文件夾
├── 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數(shù)據(jù)集
coco數(shù)據(jù)集叫Microsoft COCO(Common Objects in Context)
類別更多有91個,圖片超過300000個梳杏,而且每張圖片的類別也更多韧拒,難度更大
- MS COCO 數(shù)據(jù)集主頁:http://mscoco.org/
- Github 網(wǎng)址:https://github.com/Xinering/cocoapi
- 關于 API 更多的細節(jié)在網(wǎng)站: http://mscoco.org/dataset/#download
1、2014年數(shù)據(jù)集的下載
http://msvocds.blob.core.windows.net/coco2014/train2014.zip
2十性、2017的數(shù)據(jù)集的下載
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數(shù)據(jù)集可以直接使用api進行操作叛溢,方便調(diào)用,如sklearn的dataset一樣劲适,同時coco API也有python matlab lua等不同版本楷掉,在部署階段我們也看到下載,detr的代碼也是直接調(diào)用coco API的
coco數(shù)據(jù)集下載解壓后霞势,文件夾主要就是標注文件 annotation和圖片集烹植,一般分為train和val兩個文件夾保存;
path/to/coco/
├── annotations/ # 標注json文件
├── train2017/ # 訓練集圖片
├── val2017/ # 驗證集圖片
COCO數(shù)據(jù)集現(xiàn)在有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格式轉(zhuǎn)coco格式
和detr一樣,detr 家族的項目很多是針對coco數(shù)據(jù)集的固以,所以我們自己要轉(zhuǎn)換為coco格式墩虹。
這個很多案例和開源嘱巾,這里只是做個記錄和整合,用已有的輪子改
先將數(shù)據(jù)集劃分诫钓,可以直接用voc 的main的txt 旬昭,生成,也可以對圖片數(shù)據(jù)直接打亂抽樣菌湃,做自己的訓練和驗證集合问拘。
直接對圖片進行抽取,按比例復制到各文件夾
#!/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目錄下的惧所,然后在進行圖片和標注文件的復制轉(zhuǎn)移骤坐。最后將xml轉(zhuǎn)為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()
根據(jù)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')
轉(zhuǎn)換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))
運行只要執(zhí)行 python voc2coco sourcedir targetdir 如
python voc2coco.py ../xml/xml_train ./data/coco/instance_train2017.json
當然這些步驟可以集成到一個文件一鍵操作。
參考: https://github.com/Tony607/voc2coco
目前VOC比較常用的是2007和2012數(shù)據(jù)集纯路,但是2012沒有test或油,所以多使用這兩個組合, 可以使用 VOC2007 和 VOC2012 的 train+val(16551) 上訓練驰唬,然后使用 VOC2007 的 test(4952) 測試顶岸。
當然還要別的組合 可以參考:
https://blog.csdn.net/mzpmzk/article/details/88065416
配置更改
數(shù)據(jù)準備好了岔乔,接下來就要根據(jù)自己的數(shù)據(jù)進行代碼的修改
我們當然可以從頭訓練自己的深度學習模型席赂,但是比較難的,而且需要很多資源谴咸,所以一般使用預訓練模型搓逾,也就是已經(jīng)在別的數(shù)據(jù)集上訓練過的模型拿來再進行訓練卷谈。
如圖像的通常指的是在Imagenet上訓練的CNN
這里我們可以直接下載 detr 訓練coco數(shù)據(jù)集的模型,也就是91類的模型架構(gòu)霞篡,
下載模型 :
wget https://dl.fbaipublicfiles.com/detr/detr-r50-e632da11.pth
這是detr 訓練的 resnet50 經(jīng)典的cnn結(jié)構(gòu)
這些模型也可通過torch hub找到世蔗,以用預訓練的權重加載DETR R50,只需執(zhí)行以下操作:
model = torch.hub.load('facebookresearch/detr', 'detr_resnet50', pretrained=True)
PyTorch Hub是一個簡易API和工作流程朗兵,為復現(xiàn)研究提供了基本構(gòu)建模塊污淋,包含預訓練模型庫。并且余掖,PyTorch Hub還支持Colab寸爆,能與論文代碼結(jié)合網(wǎng)站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文件里。
更改模型結(jié)構(gòu) 粉洼,適應自己的類別個數(shù): 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
這里也說明到务嫡,要將參數(shù)設為類別+1甲抖,一個是背景類
所有準備工作基本都好了,可以開始煉丹了
這里使用的是docker 心铃,要注意因為內(nèi)存使用的比較大,而docker 有限制共享內(nèi)存 默認是64M 挫剑,所有運行docker 時候要加上參數(shù) --shm-size
將數(shù)據(jù)集掛載到代碼目錄的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數(shù)據(jù)集,當然最好用沒用過的測試集
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
參考:
http://www.reibang.com/p/d7a06a720a2b
https://github.com/DataXujing/detr_transformer
https://www.bilibili.com/video/BV1GC4y1h77h