淘先锋技术网

首页 1 2 3 4 5 6 7

参考链接:https://blog.csdn.net/mr_health/article/details/80676799

 

一、数据准备

我的数据格式是voc,而detectron要求的数据格式是json,因此首先要进行格式的转换。

1.数据放置

官方网站上的要求的数据存放见左图,也就是说无论我们自己要训练什么数据,文件夹的名称必须是VOC+year。但是为什么要是VOC+year这样的形式呢,我们再翻看一下datasets/dadataset_catalog.py这个函数(右图),可以看到里面有各种数据集,包括voc2007、voc2012。实际上这些就是数据接口,也就是说我们把自己的数据按照接口的形式准备好,detectron就会调用。

一般情况下,我们的数据集可以分为三部分,一部分是训练数据(train),第二部分验证数据(val)、第三部分是测试数据(test)。实际上验证数据和测试数据都可以被称为测试数据,只是功能有所不同。验证数据(val)由于其带标签,在测试中往往会输出mAP,用以评估模型的性能,而测试数据(test)不含有标签,实际上就是当我们训练好了模型,那就在我的一个数据库上试试呗,看检测出来好玩不,测试数据(test)就这样一个数据库。右图中voc_2007_train就是用于训练,voc_2007_val用于验证,voc_2007_test用于测试。

但是实际上在我们训练模型的时候,只将数据集分为训练数据和测试数据,只是这时候的测试数据功能上是对模型的评估,所以在代码的书写上都以val指代,称呼上就是测试数据(下文就这么称呼了)。

 

注意:只有train,val可以生成AP,AP50,mAP结果,只要包含test这些结果为-1

 

dataset_catalog.py当中的文件,按照这个文件名命名

 

 

 

 

 

 

 

 

文件结构:在/home/test/detectron/detectron/datasets/data当中新建一个文件夹:VOC2007

 

1)annotations里面有两个json文件,分别是voc_2007_train.json,voc2007_val.json

将annotations文件夹当中的xml拷贝出来,复制到train,val两个文件夹当中,利用上述代码,生成train,val这两个json文件

修改相应的路径

#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

        print(f)
        name =f.split('.')[0]
        valPath = '/home/test/detectron/detectron/datasets/data/VOC2007/VOCdevkit2007/VOC2007/ImageSets/Main/'
        valPath = os.path.join(valPath,"val.txt")
        with open(valPath,'a+') as ff:
            ff.write(name)
            ff.write('\n')

        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)#输出xml文件的全路径

        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 <folder>, <filename>, <size>, <object>
        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 <size> 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)#图片信息
                    print('add image with {} and {}'.format(file_name, size))
                else:
                    raise Exception('duplicated image: {}'.format(file_name))
                    # subelem is <width>, <height>, <depth>, <name>, <bndbox>
            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 <xmin>, <ymin>, <xmax>, <ymax>, when subelem is <bndbox>
                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 <object> 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__':
    # / home / test / detectron / detectron / datasets / data / VOC2007 / VOCdevkit2007 / VOC2007 / Annotations
    xml_path = '/home/test/detectron/detectron/datasets/data/VOC2007/VOCdevkit2007/VOC2007/Annotations/'
    json_file = '/home/test/detectron/detectron/datasets/data/VOC2007/annotations/voc_2007_val.json'
    parseXmlFiles(xml_path)
    json.dump(coco, open(json_file, 'w'))

2)JPEGImages里就是所有的训练的数据

3)VOCdevkit2007就是将pascal voc 2007直接解压得到的结果,将名字由VOCdevkit改为VOCdevkit2007

 

 

创建一个VOC2007的文件夹,将数据从VOCdevkit/VOC2007/JPEGImages中取出来放到VOC2007/JPEGImages中,将训练数据和测试数据的xml文件进行划分,分别放置在train_Annotations和val_Annotations中。同时可以创建一个Annotations文件夹,用来存放转换后的json文件,也就是上图中的annotations。由于我选择的year=2007,因此转换后的训练数据和测试数据文件名称分别为:voc_2007_train.json,voc_2007_val.json。

 

最后创建VOCdevkit2007文件夹,其结构如下(里面还有一个VOC2007文件),注意其中的ImageSets/Main目录中存放的是自己数据的train.txt和val.txt,文件内容是图片名称的前缀,也就是6位数字

 

 

二、模型训练

在/home/yantianwang/clone/detectron中创建一个文件夹experiments,用于存放模型,配置文件(yaml文件),以及最后训练核测试后的结果。

这里我采用Retianet50+FPN来训练自己的数据

1.下载模型

官方下载地址:model-zoo

下载R-50.pkl到experiments文件夹中

 

2.设置配置文件

将configs/12_2017_baselines中的retinanet_R-50-FPN_1x.yaml拷贝到experiments文件中,主要修改

NUM_CLASSES: 你自己的类别数+1(因为有背景)

MAX_ITER:迭代次数

TRAIN:

           WEIGHTS: /home/yantianwang/clone/detectron/experiments/R-50.pkl (因为我习惯在spyder中运行,所以路径是绝对路径)

            DATASETS: ('voc_2007_train',)   也就是修改为我们刚刚转换格式后json文件

TEST:

            DATASETS: ('voc_2007_val',)     修改成我们的val,训练完毕后会自动进行测试,输出mAP

OUTPUT_DIR: /home/yantianwang/clone/detectron/experiments/result     训练结果和测试结果的存放路径

注意,训练中很多参数是可以修改的,见detectron/core/config.py,如果你要修改的话,不用在py文件中,直接在这个.yaml文件中的相应位置写出参数名字,后面跟上修改的数值即可。例如我想修改:

该参数是归在TRAIN下的(因为是C.TRAIN) ,因此我在.yaml文件中的TRAIN下作如下添加即可:

 

最终修改结果:

MODEL:
  TYPE: retinanet
  CONV_BODY: FPN.add_fpn_ResNet50_conv5_body
#设置总共的类别,前景+背景,20+1
  NUM_CLASSES: 21
NUM_GPUS: 1
SOLVER:
  WEIGHT_DECAY: 0.0001
  LR_POLICY: steps_with_decay
  #基础学习率必须调小一些,否则loss会出现为0的情况
  BASE_LR: 0.00001
  GAMMA: 0.1
#最大迭代次数
  MAX_ITER: 5000
  STEPS: [0, 60000, 80000]
FPN:
  FPN_ON: True
  MULTILEVEL_RPN: True
  RPN_MAX_LEVEL: 7
  RPN_MIN_LEVEL: 3
  COARSEST_STRIDE: 128
  EXTRA_CONV_LEVELS: True
RETINANET:
  RETINANET_ON: True
  NUM_CONVS: 4
  ASPECT_RATIOS: (1.0, 2.0, 0.5)
  SCALES_PER_OCTAVE: 3
  ANCHOR_SCALE: 4
  LOSS_GAMMA: 2.0
  LOSS_ALPHA: 0.25
TRAIN:
#基础网络,resnet50
  WEIGHTS: /home/test/detectron/experiments/R-50.pkl
  #也就是修改为我们刚刚转换格式后json文件

  DATASETS: ('voc_2007_train',)
  SCALES: (800,)
  MAX_SIZE: 1333
  RPN_STRADDLE_THRESH: -1  # default 0
TEST:
  #也就是修改为我们刚刚转换格式后json文件
  DATASETS: ('voc_2007_val',)
  SCALE: 800
  MAX_SIZE: 1333
  NMS: 0.5
  RPN_PRE_NMS_TOP_N: 10000  # Per FPN level
  RPN_POST_NMS_TOP_N: 2000
#将结果保存位置
OUTPUT_DIR: /home/test/detectron/experiments/result

 

3.开始训练

 

用spyder打开Detectron/tools/train_net.py文件,修改parse_args()函数:

注释掉 :

if len(sys.argv) == 1:
        parser.print_help()
        sys.exit(1)

修改:

 parser.add_argument(
        '--cfg',
        dest='cfg_file',
        help='Config file for training (and optionally testing)',
        default= '/home/yantianwang/clone/detectron/experiments/retinanet_R-50-FPN_1x.yaml',   #配置文件的绝对路径
        type=str

修改完毕点击运行就可以了

注意:如果你不想利用spyder进行调试,而只是想通过下面的命令终端运行,那么就不用修改train_net.py,同时.yaml配置文件中的路径也统统修改为相对路径。

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

4.对于训练结果的评估

在训练之后,会对于训练结果进行AP值的测评,此时有个坑:在每次训练完之后会在detectron/detectron/datasets/data/VOC2007/VOCdevkit2007/annotations_cache当中生成一个cache文件,如果不删除该文件夹当中的文件,那么会在下次验证的时候报错。

 

detectron截至2018年9月18日为止,仅仅支持类coco数据集的评估,不支持voc格式的评估,如果数据集前面含有VOC_这个前缀,就会调用_voc_eval_to_box_results函数,然而该函数默认初始值,从而导致AP值为-1

 

 

 

 修改方法:在dataset_category.py文件中将val数据集改为含有coco字符,使其进入coco数据集的测评函数中,并且验证的json文件中不能含有test字样,否则AP值都会为-1

 

 

 最终可以得到一个AP值不为-1的结果