1. 程式人生 > 其它 >VOC資料集和COCO資料集直接的相互轉換

VOC資料集和COCO資料集直接的相互轉換

VOC資料集(xml格式)和COCO資料集(json格式)的相互轉換

我們先來看看voc和coco資料集的目錄結構:
以VOC2012資料集為例,下載下來有如下五個資料夾:
在這裡插入圖片描述
Annotations資料夾是存放圖片對應的xml檔案,比如“2007_000027.xml"存放的是圖片2007_000027.jpg對應的資訊,用記事本開啟可以看到,這是xml格式的資料。
ImageSets資料夾裡存放了官方為我們劃分好的訓練集和驗證集的txt檔案。我們主要使用“ImageSets/Main/"資料夾下的train.txt和val.txt檔案,train.txt檔案存放了官方劃分的訓練集的圖片名稱,val.txt檔案存放了驗證集圖片的名稱。

還有一個需要關注的資料夾就是JEPGImages,裡面存放了對應圖片名稱的原始圖片。剩下的兩個資料夾我們就不需要特別關注了。

接下來我們來看看voc資料集的xml檔案裡面都有哪些資訊。

<annotation>
    <folder>資料夾目錄</folder>
    <filename>圖片名.jpg</filename>
    <path>path_to\at002eg001.jpg</path>
    <source>
        <database>Unknown</database>
</source> <size> <width>550</width> <height>518</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>Apple</name> <pose>Unspecified</pose>
<truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>292</xmin> <ymin>218</ymin> <xmax>410</xmax> <ymax>331</ymax> </bndbox> </object> <object> ... </object> </annotation>

可以看到一個xml檔案包含如下資訊:

  • folder: 資料夾
  • filename:檔名
  • path:路徑
  • source:來源
  • size:圖片大小
  • segmented:影象分割會用到,本文僅以目標檢測(bounding box為例進行介紹)
  • object:一個xml檔案可以有多個object,每個object表示一個box,每個box有如下資訊組成:
  • name:改box框出來的object屬於哪一類,例如Apple
  • bndbox:給出左上角和右下角的座標
  • truncated:是否被截
  • difficult:是否為檢測困難物體

不同於VOC,一張圖片對應一個xml檔案,coco是直接將所有圖片以及對應的box資訊寫在了一個json檔案裡。通常整個coco目錄長這樣:

coco
|______annotations # 存放標註資訊
|        |__train.json
|        |__val.json
|        |__test.json
|______trainset # 存放訓練集影象
|______valset   # 存放驗證集影象
|______testset  # 存放測試集影象

一個標準的json檔案包含如下資訊:

{ 
    "info" : info,
    "licenses" : [license],
    "images" : [image],
    "annotations" : [annataton],
    "categories" : [category]
}

通過上面的json整體結構可以看出,info這個key對應的值的型別是一個字典;licenses、images、annotations和categories這四個key對應的值的型別都是一個列表,列表當中儲存的資料型別依舊是字典。
我們可以通過len(List)的方式得到images、annotations、categories這三個列表的長度,也就得到了以下內容。

(1)images欄位列表元素的長度 = 劃入訓練集(或者測試集)的圖片的數量;
(2)annotations欄位列表元素的數量 = 訓練集(或者測試集)中bounding box的數量;
(3)categories欄位列表元素的數量 = 類別的數量

接下來我們看每個key對應的內容:

(1)info

info{
"year" : int,                # 年份
"version" : str,             # 版本
"description" : str,         # 詳細描述資訊
"contributor" : str,         # 作者
"url" : str,                 # 協議連結
"date_created" : datetime,   # 生成日期
}

(2)images

"images": [                                            
{"id": 0,                                                # int 影象id,可從0開始
 "file_name": "0.jpg",                                   # str 檔名
 "width": 512,                                           # int 影象的寬
 "height": 512,                                          # int 影象的高
 "date_captured": "2020-04-14 01:45:07.508146",          # datatime 獲取日期
 "license": 1,                                           # int 遵循哪個協議
 "coco_url": "",                                         # str coco圖片連結url
 "flickr_url": ""                                        # str flick圖片連結url
}]

(3)licenses

 "licenses": [
{
 "id": 1,                                            # int 協議id號      在images中遵循的license即1
 "name": null,                                       # str 協議名        
 "url": null                                         # str 協議連結      
}]

(4)annotations

"annotations": [ 
{
 "id": 0,                                   # int 圖片中每個被標記物體的id編號
 "image_id": 0,                             # int 該物體所在圖片的編號
 "category_id": 2,                          # int 被標記物體的類別id編號
 "iscrowd": 0,                              # 0 or 1 目標是否被遮蓋,預設為0
 "area": 4095.9999999999986,                # float 被檢測物體的面積(64 * 64 = 4096)
 "bbox": [200.0, 416.0, 64.0, 64.0],        # [x, y, width, height] 目標檢測框的座標資訊
 "segmentation": [[200.0, 416.0, 264.0, 416.0, 264.0, 480.0, 200.0, 480.0]]  
}]

"bbox"裡[x, y, width, height]x, y代表的是物體的左上角的x, y的座標值。

"segmentation"裡[x1, y1, x2, y2, x3, y3, x4, y4]是以左上角座標為起始,順時針依次選取的另外三個座標點。及[左上x, 左上y, 右上x,右上y,右下x,右下y,左下x,左下y]。

(5)categories

"categories":[
{
 "id": 1,                                 # int 類別id編號
 "name": "rectangle",                     # str 類別名字
 "supercategory": "None"                  # str 類別所屬的大類,如卡車和轎車都屬於機動車這個class
}, 
{
 "id": 2,
 "name": "circle", 
 "supercategory": "None"
 }
]

一、將voc資料集的xml轉化為coco資料集的json格式

GitHub開源專案地址

開始轉換前,得先將要轉化的所有.xml檔名儲存在xml_list.txt列表中。如果是自己製作的voc資料集,在輸入標籤名的時候記得不要把類別名name打錯了。

# create_xml_list.py
import os
xml_list = os.listdir('C:/Users/user/Desktop/train')
with open('C:/Users/user/Desktop/xml_list.txt','a') as f:
    for i in xml_list:
        if i[-3:]=='xml':
            f.write(str(i)+'\n')

執行python voc2coco.py xml_list.txt的檔案路徑 .xml檔案的真實存放路徑 轉化後的.json存放路徑即可將xml轉化為一個.json檔案。

# voc2coco.py

# pip install lxml

import sys
import os
import json
import xml.etree.ElementTree as ET

START_BOUNDING_BOX_ID = 1
PRE_DEFINE_CATEGORIES = {}
# 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 NotImplementedError('Can not find %s in %s.'%(name, root.tag))
    if length > 0 and len(vars) != length:
        raise NotImplementedError('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 = os.path.splitext(filename)[0]
        return int(filename)
    except:
        raise NotImplementedError('Filename %s is supposed to be an integer.'%(filename))


def convert(xml_list, xml_dir, json_file):
    list_fp = open(xml_list, 'r')
    json_dict = {"images":[], "type": "instances", "annotations": [],
                 "categories": []}
    categories = PRE_DEFINE_CATEGORIES
    bnd_id = START_BOUNDING_BOX_ID
    for line in list_fp:
        line = line.strip()
        print("Processing %s"%(line))
        xml_f = os.path.join(xml_dir, line)
        tree = ET.parse(xml_f)
        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 NotImplementedError('%d paths found in %s'%(len(path), line))
        ## The filename must be a number
        image_id = get_filename_as_int(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)
        ## Cruuently 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(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)
    json_fp = open(json_file, 'w')
    json_str = json.dumps(json_dict)
    json_fp.write(json_str)
    json_fp.close()
    list_fp.close()


if __name__ == '__main__':
    if len(sys.argv) <= 1:
        print('3 auguments are need.')
        print('Usage: %s XML_LIST.txt XML_DIR OUTPU_JSON.json'%(sys.argv[0]))
        exit(1)

    convert(sys.argv[1], sys.argv[2], sys.argv[3])

注意這裡的image_id用的是圖片名稱去掉.jpg,所以圖片名必須是數字,如果不是,先將所有圖片和label名稱改成數字,再轉coco。

import os
img_dir='F:/Billboard/dataset/images/'
lab_dir='F:/Billboard/dataset/labels/'
name_list = os.listdir(img_dir)
for i,name in enumerate(name_list):
    os.rename(img_dir+name,img_dir+str(i)+'.jpg')
    os.rename(lab_dir+name[:-4]+'.txt',lab_dir+str(i)+'.txt')

二、將COCO格式的json檔案轉化為VOC格式的xml檔案

如果是要將COCO格式的json檔案轉化為VOC格式的xml檔案,將anno和xml_dir改成json檔案路徑和轉化後的xml檔案儲存路徑,執行下面程式碼即可完成轉化。

# coco2voc.py

# pip install pycocotools
import os
import time
import json
import pandas as pd
from tqdm import tqdm
from pycocotools.coco import COCO
 
#json檔案路徑和用於存放xml檔案的路徑
anno = 'C:/Users/user/Desktop/val/instances_val2017.json'
xml_dir = 'C:/Users/user/Desktop/val/xml/'

coco = COCO(anno)  # 讀檔案
cats = coco.loadCats(coco.getCatIds())  # 這裡loadCats就是coco提供的介面,獲取類別
    
# Create anno dir
dttm = time.strftime("%Y%m%d%H%M%S", time.localtime())

def trans_id(category_id):
    names = []
    namesid = []
    for i in range(0, len(cats)):
        names.append(cats[i]['name'])
        namesid.append(cats[i]['id'])
    index = namesid.index(category_id)
    return index
    

def convert(anno,xml_dir): 

    with open(anno, 'r') as load_f:
        f = json.load(load_f)
    
    imgs = f['images']  #json檔案的img_id和圖片對應關係 imgs列表表示多少張圖
    
    cat = f['categories']
    df_cate = pd.DataFrame(f['categories'])                     # json中的類別
    df_cate_sort = df_cate.sort_values(["id"], ascending=True)  # 按照類別id排序
    categories = list(df_cate_sort['name'])                     # 獲取所有類別名稱
    print('categories = ', categories)
    df_anno = pd.DataFrame(f['annotations'])                    # json中的annotation
    
    for i in tqdm(range(len(imgs))):  # 大迴圈是images所有圖片,Tqdm是可擴充套件的Python進度條,可以在長迴圈中新增一個進度提示資訊
        xml_content = []
        file_name = imgs[i]['file_name']    # 通過img_id找到圖片的資訊
        height = imgs[i]['height']
        img_id = imgs[i]['id']
        width = imgs[i]['width']
        
        version =['"1.0"','"utf-8"'] 
    
        # xml檔案新增屬性
        xml_content.append("<?xml version=" + version[0] +" "+ "encoding="+ version[1] + "?>")
        xml_content.append("<annotation>")
        xml_content.append("    <filename>" + file_name + "</filename>")
        xml_content.append("    <size>")
        xml_content.append("        <width>" + str(width) + "</width>")
        xml_content.append("        <height>" + str(height) + "</height>")
        xml_content.append("        <depth>"+ "3" + "</depth>")
        xml_content.append("    </size>")
    
        # 通過img_id找到annotations
        annos = df_anno[df_anno["image_id"].isin([img_id])]  # (2,8)表示一張圖有兩個框
    
        for index, row in annos.iterrows():  # 一張圖的所有annotation資訊
            bbox = row["bbox"]
            category_id = row["category_id"]
            cate_name = categories[trans_id(category_id)]
    
            # add new object
            xml_content.append("    <object>")
            xml_content.append("        <name>" + cate_name + "</name>")
            xml_content.append("        <truncated>0</truncated>")
            xml_content.append("        <difficult>0</difficult>")
            xml_content.append("        <bndbox>")
            xml_content.append("            <xmin>" + str(int(bbox[0])) + "</xmin>")
            xml_content.append("            <ymin>" + str(int(bbox[1])) + "</ymin>")
            xml_content.append("            <xmax>" + str(int(bbox[0] + bbox[2])) + "</xmax>")
            xml_content.append("            <ymax>" + str(int(bbox[1] + bbox[3])) + "</ymax>")
            xml_content.append("        </bndbox>")
            xml_content.append("    </object>")
        xml_content.append("</annotation>")
    
        x = xml_content
        xml_content = [x[i] for i in range(0, len(x)) if x[i] != "\n"]
        ### list存入檔案
        #xml_path = os.path.join(xml_dir, file_name.replace('.xml', '.jpg'))
        xml_path = os.path.join(xml_dir, file_name.split('j')[0]+'xml')
        print(xml_path)
        with open(xml_path, 'w+', encoding="utf8") as f:
            f.write('\n'.join(xml_content))
        xml_content[:] = []

if __name__ == '__main__':
    convert(anno,xml_dir)

三、將 txt 檔案轉換為 Pascal VOC 的 XML 格式

比如從OpenImageV5下載下來的BIllboard資料集,目錄如下:

Billboard
|______images # 存放訓練集影象
|        |__train
		       |__train.jpg
|        |__val
               |__val.jpg
|______labels # 存放標註資訊
|        |__train
		       |__train.txt
|        |__val
               |__val.txt

每一副影象所對應的txt裡面的內容對應目標的座標資訊。這裡0後面四個值為用長寬歸一化了的x1,x2,y1,y2。如下圖所示,0表示只有billboard一個類別。
在這裡插入圖片描述

將 txt 檔案轉換為 Pascal VOC 的 XML 格式的程式碼如下:

#! /usr/bin/python
# -*- coding:UTF-8 -*-
import os, sys
import glob
from PIL import Image
 
# VEDAI 影象儲存位置
src_img_dir = "F:/Billboard/dataset/images/val"
# VEDAI 影象的 ground truth 的 txt 檔案存放位置
src_txt_dir = "F:/Billboard/dataset/labels/val"
src_xml_dir = "F:/Billboard/dataset/xml/val"
name=['billboard']
 
img_Lists = glob.glob(src_img_dir + '/*.jpg')
 
img_basenames = [] # e.g. 100.jpg
for item in img_Lists:
    img_basenames.append(os.path.basename(item))
 
img_names = [] # e.g. 100
for item in img_basenames:
    temp1, temp2 = os.path.splitext(item)
    img_names.append(temp1)
 
for img in img_names:
    im = Image.open((src_img_dir + '/' + img + '.jpg'))
    width, height = im.size
 
    # open the crospronding txt file
    gt = open(src_txt_dir + '/' + img + '.txt').read().splitlines()
    #gt = open(src_txt_dir + '/gt_' + img + '.txt').read().splitlines()
 
    # write in xml file
    #os.mknod(src_xml_dir + '/' + img + '.xml')
    xml_file = open((src_xml_dir + '/' + img + '.xml'), 'w')
    xml_file.write('<annotation>\n')
    xml_file.write('    <folder>VOC2007</folder>\n')
    xml_file.write('    <filename>' + str(img) + '.jpg' + '</filename>\n')
    xml_file.write('    <size>\n')
    xml_file.write('        <width>' + str(width) + '</width>\n')
    xml_file.write('        <height>' + str(height) + '</height>\n')
    xml_file.write('        <depth>3</depth>\n')
    xml_file.write('    </size>\n')
 
    # write the region of image on xml file
    for img_each_label in gt:
        spt = img_each_label.split(' ') #這裡如果txt裡面是以逗號‘,’隔開的,那麼就改為spt = img_each_label.split(',')。
        xml_file.write('    <object>\n')
        xml_file.write('        <name>' + str(name[int(spt[0])]) + '</name>\n')
        xml_file.write('        <pose>Unspecified</pose>\n')
        xml_file.write('        <truncated>0</truncated>\n')
        xml_file.write('        <difficult>0</difficult>\n')
        xml_file.write('        <bndbox>\n')
        xml_file.write('            <xmin>' + str(float(spt[1])*width) + '</xmin>\n')
        xml_file.write('            <ymin>' + str(float(spt[3])*height) + '</ymin>\n')
        xml_file.write('            <xmax>' + str(float(spt[2])*width) + '</xmax>\n')
        xml_file.write('            <ymax>' + str(float(spt[4])*height) + '</ymax>\n')
        xml_file.write('        </bndbox>\n')
        xml_file.write('    </object>\n')
 
    xml_file.write('</annotation>')

至此,我們基本能夠應對目標檢測常用到的資料轉化了,不管我們拿到的是什麼資料集,VOC也好,COCO也罷,亦或是各種txt格式,我們都可以用上面的方法轉成我們所需要的資料集。至於自己製作資料集,也很簡單,篇幅有限,下一篇再總結了。。。