Взаимопреобразование между форматами данных VOC и YOLO

глубокое обучение

Это пятый день моего участия в августовском испытании обновлений, подробности о мероприятии:Испытание августовского обновления

окрестности

  • виндовс 10 64 бит
  • python 3.7
  • labelImg

Аннотация к изображению

Здесь используются инструменты с открытым исходным кодомlabelImgАннотируйте изображение, и формат экспортируемого набора данныхPASCAL VOC, после завершения маркировки данных вы можете видеть, что папка выглядит так, маркируйте файлxmlсмешанный с файлами изображений

yolo voc

Самодельный набор данных VOC

Во-первых, согласноVOC2007Требования к формату набора данных, создавать папки отдельноVOCdevkit,VOC2007,Annotations,ImageSets,MainиJPEGImages, их иерархия следующая

└─VOCdevkit
    └─VOC2007
        ├─Annotations
        ├─ImageSets
        │  └─Main
        └─JPEGImages

в,Annotationsиспользуется для храненияxmlфайл аннотации,JPEGImagesиспользуется для хранения файлов изображений иImageSets/Mainхранить несколькоtxtТекстовый файл.Содержимое файла - это имена изображений в тренировочном наборе, проверочном наборе и тестовом наборе (уберите расширение).Эти текстовые файлы необходимо сгенерировать самим, о чем мы поговорим позже.

Далее будетimagesСкопируйте файлы изображений из папки вJPEGImagesпапка,imagesв файлеxmlСкопируйте файл аннотации вAnnotationsв папке

Затем создайте новый скрипт и поместите его вVOCdevkit/VOC2007папка, названнаяtest.py

─VOCdevkit
    └─VOC2007
        │  test.py
        │
        ├─Annotations
        ├─ImageSets
        │  └─Main
        └─JPEGImages

Содержание скрипта следующее

import os
import random

# 训练集和验证集的比例分配
trainval_percent = 0.1
train_percent = 0.9

# 标注文件的路径
xmlfilepath = 'Annotations'

# 生成的txt文件存放路径
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/trainval.txt', 'w')
ftest = open('ImageSets/Main/test.txt', 'w')
ftrain = open('ImageSets/Main/train.txt', 'w')
fval = open('ImageSets/Main/val.txt', 'w')

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

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

Затем перейдите в каталогVOCdevkit/VOC2007, выполнить скрипт, после завершения, вImageSets/Mainсгенерировано 4txtдокумент

├─ImageSets
│  └─Main
│          test.txt
│          train.txt
│          trainval.txt
│          val.txt
│
└─JPEGImages

Формат этих четырех файлов одинаков, а содержимое файла — это соответствующее имя изображения без расширения (сxmlУдалить файл аннотации.xmlпоследовательные) результаты

yolo voc

OK, с вышеуказанной подготовкой данных, и, наконец, мы используемYOLOсерединаv3/v4версию в качестве примера, чтобы увидеть, как сочетаются набор данных и профиль обучения?

Здесь мы загружаем файл изyoloофициальный файл скриптаСемья P Eddie.com/Media/files…,ПучокurlПодать заявку в браузере для скачивания

Код относительно прост, то есть абсолютный путь изображения потребуется обучить, проверить и записать соответствующую картинку.txtв файле

import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join

# 原始脚本中包含了VOC2012,这里,我们把它删除
# sets=[('2012', 'train'), ('2012', 'val'), ('2007', 'train'), ('2007', 'val'), ('2007', 'test')]
sets=[('2007', 'train'), ('2007', 'val'), ('2007', 'test')]

# classes也需要根据自己的实际情况修改
# classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
classes = ["hat"]


def convert(size, box):
    dw = 1./size[0]
    dh = 1./size[1]
    x = (box[0] + box[1])/2.0
    y = (box[2] + box[3])/2.0
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x*dw
    w = w*dw
    y = y*dh
    h = h*dh
    return (x,y,w,h)

def convert_annotation(year, image_id):
    in_file = open('VOCdevkit/VOC%s/Annotations/%s.xml'%(year, image_id))
    out_file = open('VOCdevkit/VOC%s/labels/%s.txt'%(year, image_id), 'w')
    tree=ET.parse(in_file)
    root = tree.getroot()
    size = root.find('size')
    w = int(size.find('width').text)
    h = int(size.find('height').text)

    for obj in root.iter('object'):
        difficult = obj.find('difficult').text
        cls = obj.find('name').text
        if cls not in classes or int(difficult) == 1:
            continue
        cls_id = classes.index(cls)
        xmlbox = obj.find('bndbox')
        b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text))
        bb = convert((w,h), b)
        out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')

wd = getcwd()

for year, image_set in sets:
    if not os.path.exists('VOCdevkit/VOC%s/labels/'%(year)):
        os.makedirs('VOCdevkit/VOC%s/labels/'%(year))
    image_ids = open('VOCdevkit/VOC%s/ImageSets/Main/%s.txt'%(year, image_set)).read().strip().split()
    list_file = open('%s_%s.txt'%(year, image_set), 'w')
    for image_id in image_ids:
        list_file.write('%s/VOCdevkit/VOC%s/JPEGImages/%s.jpg\n'%(wd, year, image_id))
        convert_annotation(year, image_id)
    list_file.close()

После выполнения вышеуказанного скрипта вVOCdevkitБудет создан каталог того же уровня2007_train.txt,2007_val.txt,2007_test.txt.

yolo voc

вот и все, домашнееVOC2007Набор данных готов. соответствуетdarknetфайл конфигурации вcfg/voc.dataпросто пиши

classes= 1
train  = 2007_train.txt
valid  = 2007_val.txt
names = data/voc.names
backup = backup/

Преобразование в формат данных YOLO

Во-первых, позвольте мне объяснить, что ранее упомянутые инструменты аннотацииlabelImgможно экспортироватьYOLOФормат данных. Но если вы понимаете, формат этикеткиxmlДанные, его необходимо преобразовать. Возьми пример, который мы отмечали выше

хранить все изображения вimagesпапка,xmlФайлы аннотаций помещаются вAnnotationsпапку, затем создайте папкуlabels

├─Annotations
├─images
└─labels

Подготовьте скрипт конвертации нижеvoc2yolo.py, некоторые комментарии прописаны в коде

import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join

# 根据自己情况修改
classes = ["hat"]


def convert(size, box):
    dw = 1. / size[0]
    dh = 1. / size[1]
    x = (box[0] + box[1]) / 2.0
    y = (box[2] + box[3]) / 2.0
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x * dw
    w = w * dw
    y = y * dh
    h = h * dh
    return (x, y, w, h)


def convert_annotation(image_id):

    if not os.path.exists('Annotations/%s.xml' % (image_id)):
        return

    in_file = open('annotations/%s.xml' % (image_id))

    out_file = open('labels/%s.txt' % (image_id), 'w')
    tree = ET.parse(in_file)
    root = tree.getroot()
    size = root.find('size')
    w = int(size.find('width').text)
    h = int(size.find('height').text)

    for obj in root.iter('object'):
        cls = obj.find('name').text
        if cls not in classes:
            continue
        cls_id = classes.index(cls)
        xmlbox = obj.find('bndbox')
        b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
             float(xmlbox.find('ymax').text))
        bb = convert((w, h), b)
        out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')

for image in os.listdir('images'):
    # 这里需要根据你的图片情况进行对应修改。比如图片名称是123.456.jpg,这里就会出错了。一般来讲,如果图片格式固定,如全都是jpg,那就image_id=image[:-4]处理就好了。总之,情况比较多,自己看着办,哈哈!
    image_id = image.split('.')[0]
    convert_annotation(image_id)

После выполнения вышеуказанного скриптаlabelsпапка будет созданаtxtформатированный файл аннотации

все знают,yolov5Структура набора данных, используемая во время обучения, выглядит следующим образом.

├─test
│  ├─images
│  └─labels
├─train
│  ├─images
│  └─labels
└─valid
    ├─images
    └─labels

Поэтому нам также нужно объединить файл изображения с соответствующимtxtФайл этикетки снова разделяется, и сначала создается внешний слой.train,valid,testпапку, а затем создать отдельную папку под каждой папкойimagesиlabelsпапка

Затем вы можете использовать следующий скрипт, чтобы пропорционально разделить файлы изображений и меток.

import os
import shutil
import random

# 训练集、验证集和测试集的比例分配
test_percent = 0.1
valid_percent = 0.2
train_percent = 0.7

# 标注文件的路径
image_path = 'images'
label_path = 'labels'

images_files_list = os.listdir(image_path)
labels_files_list = os.listdir(label_path)
print('images files: {}'.format(images_files_list))
print('labels files: {}'.format(labels_files_list))
total_num = len(images_files_list)
print('total_num: {}'.format(total_num))

test_num = int(total_num * test_percent)
valid_num = int(total_num * valid_percent)
train_num = int(total_num * train_percent)

# 对应文件的索引
test_image_index = random.sample(range(total_num), test_num)
valid_image_index = random.sample(range(total_num), valid_num) 
train_image_index = random.sample(range(total_num), train_num)

for i in range(total_num):
    print('src image: {}, i={}'.format(images_files_list[i], i))
    if i in test_image_index:
        # 将图片和标签文件拷贝到对应文件夹下
        shutil.copyfile('images/{}'.format(images_files_list[i]), 'test/images/{}'.format(images_files_list[i]))
        shutil.copyfile('labels/{}'.format(labels_files_list[i]), 'test/labels/{}'.format(labels_files_list[i]))
    elif i in valid_image_index:
        shutil.copyfile('images/{}'.format(images_files_list[i]), 'valid/images/{}'.format(images_files_list[i]))
        shutil.copyfile('labels/{}'.format(labels_files_list[i]), 'valid/labels/{}'.format(labels_files_list[i]))
    else:
        shutil.copyfile('images/{}'.format(images_files_list[i]), 'train/images/{}'.format(images_files_list[i]))
        shutil.copyfile('labels/{}'.format(labels_files_list[i]), 'train/labels/{}'.format(labels_files_list[i]))

После выполнения кода вы можете увидеть аналогичную файловую иерархию

─test
│  ├─images
│  │      1234565343231.jpg
│  │      1559035146628.jpg
│  │      2019032210151.jpg
│  │
│  └─labels
│          1234565343231.txt
│          1559035146628.txt
│          2019032210151.txt
│
├─train
│  ├─images
│  │      1213211.jpg
│  │      12i4u33112.jpg
│  │      1559092537114.jpg
│  │
│  └─labels
│          1213211.txt
│          12i4u33112.txt
│          1559092537114.txt
│
└─valid
    ├─images
    │      120131247621.jpg
    │      124iuy311.jpg
    │      1559093141383.jpg
    │
    └─labels
            120131247621.txt
            124iuy311.txt
            1559093141383.txt

На данный момент набор данных действительно готов.

YOLO в VOC

если вы получитеtxt, но нужно использоватьVOC, также необходимо преобразовать. Посмотрите на скрипт ниже, комментарии прописаны в коде

import os
import xml.etree.ElementTree as ET
from PIL import Image
import numpy as np

# 图片文件夹,后面的/不能省
img_path = 'images/'

# txt文件夹,后面的/不能省
labels_path = 'labels/'

# xml存放的文件夹,后面的/不能省
annotations_path = 'Annotations/'

labels = os.listdir(labels_path)

# 类别
classes = ["hat"]

# 图片的高度、宽度、深度
sh = sw = sd = 0

def write_xml(imgname, sw, sh, sd, filepath, labeldicts):
    '''
    imgname: 没有扩展名的图片名称
    '''

    # 创建Annotation根节点
    root = ET.Element('Annotation')

    # 创建filename子节点,无扩展名                 
    ET.SubElement(root, 'filename').text = str(imgname)        

    # 创建size子节点 
    sizes = ET.SubElement(root,'size')                                      
    ET.SubElement(sizes, 'width').text = str(sw)
    ET.SubElement(sizes, 'height').text = str(sh)
    ET.SubElement(sizes, 'depth').text = str(sd) 

    for labeldict in labeldicts:
        objects = ET.SubElement(root, 'object')                 
        ET.SubElement(objects, 'name').text = labeldict['name']
        ET.SubElement(objects, 'pose').text = 'Unspecified'
        ET.SubElement(objects, 'truncated').text = '0'
        ET.SubElement(objects, 'difficult').text = '0'
        bndbox = ET.SubElement(objects,'bndbox')
        ET.SubElement(bndbox, 'xmin').text = str(int(labeldict['xmin']))
        ET.SubElement(bndbox, 'ymin').text = str(int(labeldict['ymin']))
        ET.SubElement(bndbox, 'xmax').text = str(int(labeldict['xmax']))
        ET.SubElement(bndbox, 'ymax').text = str(int(labeldict['ymax']))
    tree = ET.ElementTree(root)
    tree.write(filepath, encoding='utf-8')


for label in labels:
    with open(labels_path + label, 'r') as f:
        img_id = os.path.splitext(label)[0]
        contents = f.readlines()
        labeldicts = []
        for content in contents:
            # 这里要看你的图片格式了,我这里是jpg,注意修改
            img = np.array(Image.open(img_path + label.strip('.txt') + '.jpg'))

            # 图片的高度和宽度
            sh, sw, sd = img.shape[0], img.shape[1], img.shape[2]
            content = content.strip('\n').split()
            x = float(content[1])*sw
            y = float(content[2])*sh
            w = float(content[3])*sw
            h = float(content[4])*sh

            # 坐标的转换,x_center y_center width height -> xmin ymin xmax ymax
            new_dict = {'name': classes[int(content[0])],
                        'difficult': '0',
                        'xmin': x+1-w/2,                     
                        'ymin': y+1-h/2,
                        'xmax': x+1+w/2,
                        'ymax': y+1+h/2
                        }
            labeldicts.append(new_dict)
        write_xml(img_id, sw, sh, sd, annotations_path + label.strip('.txt') + '.xml', labeldicts)

Выполните приведенный выше скрипт, вы можетеAnnotationsувидеть преобразованныйxmlфайл. НазадVOCОперации с наборами данных см. во второй части текста.