Detectron:用自己的数据集(VOC格式)训练RetinaNet

一、 数据集(VOC)

Detectron天然支持COCO格式的数据集,但是可能我们手中的数据集是VOC格式的,这就需要我们手动的对VOC数据集进行相应的转换(xml→json),下面就以我自己的一个数据集为例,逐步说明一下数据集的配置过程。

1. 检查数据集(如果确定自己的数据集没有问题,可以跳过这一步)

首先,我们应当先检查自己的数据集是否完整,这一步也很重要,因为我就是在这一步中浪费了很长的时间。每一次应用新的数据集之前先检查一下数据集的完整性和准确性是一个很好的习惯,这会为我们省去很多解决莫名bug的时间。

由于我的数据集是从多个小数据集整合成的,所以我遇到了几个问题:

①确保每个xml标注文件都有一个相应的图像文件,即标注和图像有一一对应关系。

②xml文件中的文件名必须要有后缀(.jpg等),在训练过程中会依据这个文件名来从图像文件夹中寻找对应的图像文件。

③其他数据集问题。

2. 制作VOC文件夹(以VOC2007为例)

在 ./Detectron/detectron/datasets/data 文件夹下,创建一个文件夹,命名为 VOC2007 ,然后在 VOC2007 文件夹下,创建三个文件夹,分别命名为:annotationsJPEGImagesVOCdevkit2007。下面分别来讲这三个文件夹的配置过程。

annotations:

该文件夹下存放我们训练集和验证集的标注信息,需要是json格式,而不是之前的多个xml文件。首先,将训练集(train)和验证集(val)的标注文件(xml)放入两个不同的文件夹,如下图所示。【此处的验证集相当于一个有标注测试集,不参与训练,用来评判模型的好坏】

Detectron:用自己的数据集(VOC格式)训练RetinaNet_第1张图片Detectron:用自己的数据集(VOC格式)训练RetinaNet_第2张图片

然后修改以下代码中的相应为自己的路径,执行两次,来将这两个文件夹中的标注文件转换到两个 json 文件中,(NOTE:在生成voc_2007_train.json的时候,将第127和183行注释掉,这两句代码是为了生成val.txt

最后生成三个文件:

voc_2007_train.jsonvoc_2007_val.jsonval.txt

#coding=utf-8
#fork from https://github.com/CivilNet/Gemfield/blob/master/src/python/pascal_voc_xml2json/pascal_voc_xml2json.py
import xml.etree.ElementTree as ET
import os
import json

coco = dict()
coco['images'] = []
coco['type'] = 'instances'
coco['annotations'] = []
coco['categories'] = []

category_set = dict()
image_set = set()

category_item_id = 0
image_id = 20180000000
annotation_id = 0


def addCatItem(name):
    global category_item_id
    category_item = dict()
    category_item['supercategory'] = 'none'
    category_item_id += 1
    category_item['id'] = category_item_id
    category_item['name'] = name
    coco['categories'].append(category_item)
    category_set[name] = category_item_id
    return category_item_id


def addImgItem(file_name, size):
    global image_id
    if file_name is None:
        raise Exception('Could not find filename tag in xml file.')
    if size['width'] is None:
        raise Exception('Could not find width tag in xml file.')
    if size['height'] is None:
        raise Exception('Could not find height tag in xml file.')
    image_id += 1
    image_item = dict()
    image_item['id'] = image_id
    image_item['file_name'] = file_name
    image_item['width'] = size['width']
    image_item['height'] = size['height']
    coco['images'].append(image_item)
    image_set.add(file_name)
    return image_id


def addAnnoItem(object_name, image_id, category_id, bbox):
    global annotation_id
    annotation_item = dict()
    annotation_item['segmentation'] = []
    seg = []
    # bbox[] is x,y,w,h
    # left_top
    seg.append(bbox[0])
    seg.append(bbox[1])
    # left_bottom
    seg.append(bbox[0])
    seg.append(bbox[1] + bbox[3])
    # right_bottom
    seg.append(bbox[0] + bbox[2])
    seg.append(bbox[1] + bbox[3])
    # right_top
    seg.append(bbox[0] + bbox[2])
    seg.append(bbox[1])

    annotation_item['segmentation'].append(seg)

    annotation_item['area'] = bbox[2] * bbox[3]
    annotation_item['iscrowd'] = 0
    annotation_item['ignore'] = 0
    annotation_item['image_id'] = image_id
    annotation_item['bbox'] = bbox
    annotation_item['category_id'] = category_id
    annotation_id += 1
    annotation_item['id'] = annotation_id
    coco['annotations'].append(annotation_item)


def parseXmlFiles(xml_path):
    for f in os.listdir(xml_path):
        if not f.endswith('.xml'):
            continue

        bndbox = dict()
        size = dict()
        current_image_id = None
        current_category_id = None
        file_name = None
        size['width'] = None
        size['height'] = None
        size['depth'] = None

        xml_file = os.path.join(xml_path, f)
        print(xml_file)

        tree = ET.parse(xml_file)
        root = tree.getroot() #抓根结点元素

        if root.tag != 'annotation': #根节点标签
            raise Exception('pascal voc xml root element should be annotation, rather than {}'.format(root.tag))

        # elem is , , , 
        for elem in root:
            current_parent = elem.tag
            current_sub = None
            object_name = None

            #elem.tag, elem.attrib,elem.text
            if elem.tag == 'folder':
                continue

            if elem.tag == 'filename':
                file_name = elem.text
                if file_name in category_set:
                    raise Exception('file_name duplicated')

            # add img item only after parse  tag
            elif current_image_id is None and file_name is not None and size['width'] is not None:
                if file_name not in image_set:
                    current_image_id = addImgItem(file_name, size)#图片信息

                    val_txt.write(file_name.split('.')[0] + '\n') # 在生成voc_2007_train.json时注释掉这句代码

                    print('add image with {} and {}'.format(file_name, size))
                else:
                    raise Exception('duplicated image: {}'.format(file_name))
                    # subelem is , , , , 
            for subelem in elem:
                bndbox['xmin'] = None
                bndbox['xmax'] = None
                bndbox['ymin'] = None
                bndbox['ymax'] = None

                current_sub = subelem.tag
                if current_parent == 'object' and subelem.tag == 'name':
                    object_name = subelem.text
                    if object_name not in category_set:
                        current_category_id = addCatItem(object_name)
                    else:
                        current_category_id = category_set[object_name]

                elif current_parent == 'size':
                    if size[subelem.tag] is not None:
                        raise Exception('xml structure broken at size tag.')
                    size[subelem.tag] = int(subelem.text)

                # option is , , , , when subelem is 
                for option in subelem:
                    if current_sub == 'bndbox':
                        if bndbox[option.tag] is not None:
                            raise Exception('xml structure corrupted at bndbox tag.')
                        bndbox[option.tag] = int(option.text)

                # only after parse the  tag
                if bndbox['xmin'] is not None:
                    if object_name is None:
                        raise Exception('xml structure broken at bndbox tag')
                    if current_image_id is None:
                        raise Exception('xml structure broken at bndbox tag')
                    if current_category_id is None:
                        raise Exception('xml structure broken at bndbox tag')
                    bbox = []
                    # x
                    bbox.append(bndbox['xmin'])
                    # y
                    bbox.append(bndbox['ymin'])
                    # w
                    bbox.append(bndbox['xmax'] - bndbox['xmin'])
                    # h
                    bbox.append(bndbox['ymax'] - bndbox['ymin'])
                    print(
                    'add annotation with {},{},{},{}'.format(object_name, current_image_id, current_category_id, bbox))
                    addAnnoItem(object_name, current_image_id, current_category_id, bbox)


if __name__ == '__main__':

    val_txt = open('./val.txt', 'a') # 生成一个验证集的txt,只包含文件名,在测试的时候会用到,# 在生成voc_2007_train.json时注释掉这句代码

    xml_path = './val_anno' # 更改相应路径
    json_file = './voc_2007_val.json' # 生成json文件
    parseXmlFiles(xml_path)
    json.dump(coco, open(json_file, 'w')) 
  

将这两个json文件放入 annotations 文件夹下,完成 annotations 文件夹的配置。

JPEGImages:

这个文件夹配置简单,只需要将所有的图片放入这个文件夹即可。包括训练集和验证集

VOCdevkit2007:

① 访问网址:http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCdevkit_08-Jun-2007.tar 来下载VOC开发工具包,并将其解压后的文件放入 VOCdevkit2007 文件夹中,修改 ./VOCcode 文件夹下的 VOCinit.m 中的类别信息,注意和标注文件中的名称一致。

②在 VOCdevkit2007 文件夹下新建一个文件夹,命名为 VOC2007 ,在此文件夹下,再次创建两个文件夹,分别命名为:AnnotationsImageSets

在 Annotations 文件夹下,放入所有的 验证集xml 文件。

在 ImageSets 文件夹下,新建一个文件夹,命名为 Main,将前面和 json 文件一起生成的 val.txt 放入 Main 文件夹。

至此,我们终于制作好了数据集!

整个数据集的结构应该是下面这样的:

./Detectron
|_ detectron
|  |_ datasets
|  |  |_ data
|  |  |  |_ VOC2007
|  |  |  |  |_ annotations
|  |  |  |  |  |_ voc_2007_train.json
|  |  |  |  |  |_ voc_2007_val.json
|  |  |  |  |
|  |  |  |  |_ JPEGImages
|  |  |  |  |  |_ 00001.jpg
|  |  |  |  |  |_ 00002.jpg
|  |  |  |  |  |_ 00003.jpg
|  |  |  |  |  |_ ...
|  |  |  |  |  
|  |  |  |  |_ VOCdevkit2007
|  |  |  |  |  |_ VOC2007
|  |  |  |  |  |  |_ Annotations
|  |  |  |  |  |  |  |_ 00001.xml
|  |  |  |  |  |  |  |_ 00002.xml
|  |  |  |  |  |  |  |_ 00003.xml
|  |  |  |  |  |  |  |_ ...
|  |  |  |  |  |  |  
|  |  |  |  |  |  |_ ImageSets
|  |  |  |  |  |  |  |_ Main
|  |  |  |  |  |  |  |  |_ val.txt
|  |  |  |  |  |  
|  |  |  |  |  |_ local
|  |  |  |  |  |_ results
|  |  |  |  |  |_ VOCcode
|  |  |  |  |  |_ ...

二、模型训练

在 ./Detectron/ 文件夹下创建一个文件夹,名称随意,此处命名为 My_experiments,这个文件夹用来存放 模型(.pkl)、配置文件(.yaml)和训练及测试后的相关结果。

1. 下载预训练模型

在官网MODEL_ZOO上,下载经ImageNet预训练的模型,本文下载的是 R-50.pkl ,将其放在 My_experiments 文件夹下。

2. 修改配置文件

进入 ./Detectron/configs/12_2017_baselines 文件夹,将其中的 retinanet_R-50-FPN_1x.yaml 文件拷贝到 My_experiments 文件夹下。该文件为网络的配置文件。主要修改以下几处:

①NUM_CLASSES:将其数值修改为 你的类别数+1 (因为背景算作一类)

②NUM_GPUS:修改为你使用的GPU的数量。

③BASE_LR:初始学习率,默认是0.01,我修改为了0.001

④MAX_ITER:迭代次数,建议先设置为100,跑完之后,各个输出都正常,再将其设置为你希望的迭代次数。

⑤STEPS:在迭代多少次的时候进行学习率衰减

⑥TRAIN:

                WEIGHTS: /workspace/Detectron/My_experiments/R-50.pkl 预训练模型的位置

                DATASETS: ('voc_2007_train',)

⑦TEST:

               DATASETS: ('voc_2007_val',)

3. 开始训练

在命令终端窗口中,进入Detectron目录

执行以下语句开始训练:(另外,如果后续要可视化训练过程的 loss 变化,需要在语句的末尾加上 | tee log.txt 来保存训练日志)

python2 tools/train_net.py \
    --cfg My_experiments/retinanet_R-50-FPN_1x.yaml \
    OUTPUT_DIR My_experiments/result

4. 可视化结果

Detectron默认设置为:训练结束后,自动将模型在val数据集上进行测试并保存测试结果。

我们可以通过运行 ./Detectron/tools 目录下的 visualize_results.py 来将结果可视化。

python2 tools/visualize_results.py --dataset coco_2007_val --detections ./My_experiments/result/test/coco_2007_val/retinanet/detections.pkl --first 20

 



常见问题汇总:

1. Detectron在训练中默认 80000/NUM_GPUS 次存储一次训练模型,如果我们只有一块GPU,那80000次存储一次模型可不行,所以我们可以修改 ./Detectron/detectron/core/config.py 文件中的 __C.TRAIN.SNAPSHOT_ITERS 参数来达到我们的目的。

2. 如果所使用的GPU显存不够大,可能在训练过程中抛出 Out of Memory 错误,我们可以通过减小 ./Detectron/detectron/core/config.py 文件中的 __C.TRAIN.IMS_PER_BATCH 来解决这个问题。如果还是出现这个错误,可以尝试修改 __C.TRAIN.SCALES 和 __C.TRAIN.MAX_SIZE,我没有修改过后两个参数,不知道效果如何。

3. 出现 Loss is NAN 错误,导致训练中止。这个错误可以通过减小学习率来解决,即上文中的 BASE_LR

4. 在训练完成后,Detectron 会自动测试模型在验证集上效果的好坏,由于我们的数据集是VOC格式数据集,在测试完之后的输出上,会出现 AP, AP50等都为 -1 (如下左图)的情况,不要惊慌,这是正常现象,这是由于Detectron检测到你的数据集是VOC格式,自动用常量 -1 来进行了输出。如果你只想要 AP 和 mAP 的话,可以取上面的数据作为结果(如下右图)。

    Detectron:用自己的数据集(VOC格式)训练RetinaNet_第3张图片

如果你想要这些完整的评估数据,那么按照下面的操作即可实现:

①打开./Detectron/detectron/datasets/dataset_catalog.py 文件,按照下图进行修改,即将voc_2007_val 改为 coco_2007_val

Detectron:用自己的数据集(VOC格式)训练RetinaNet_第4张图片

②打开配置文件 ./Detectron/My_experiments/retinanet_R-50-FPN_1x.yaml ,修改 TEST:DATASETS,如下图所示:

Detectron:用自己的数据集(VOC格式)训练RetinaNet_第5张图片

这样就可以让Detectron调用COCO数据集的评估方式,来输出 AP、AP50、AP75、APs等信息。

 

Reference

https://blog.csdn.net/mr_health/article/details/80676799#commentBox

https://blog.csdn.net/mdjxy63/article/details/82749652

 

你可能感兴趣的:(目标检测,Detectron)