目标检测数据集xml格式和json的转换标本 COCO数据集的格式

annotation.json文件内容,主要有四个部分需要关注。

{
    "categories": [   // 类别组,是一个列表,长度等于类别数。列表的每个元素存放的是数据集的类别,每个类别又是一个字典格式
        {
            "supercategory": "父类",  // 该项表示类别的父类 如果不是特殊情况,通常可以与name相同
            "id": 1,             // 该项表示类别的序号
            "name": "类别1的名字"  // 该项表示类别的名字 
        },
          {
            "supercategory": "父类",  
            "id": 2,             
            "name": "类别2"  
        }
    ],
    
      "images":[   // 图像组,也是一个字典组成的列表。 长度是数据集中的图像数量
    {
            "file_name": "img_0175153.jpg",  // 该项表示图像文件名
            "width": 4096,  // 图像宽高
            "id": 1,         // 图像的id序号,这个序号是人为规定的,比如,有n张图,id从1变化到n
            "height": 3000
        },
        {
            "file_name": "img_0172993.jpg",
            "width": 4096,
            "id": 2,
            "height": 3000
        }
    ],

 "annotations": [  // 标注部分 也是字典构成的列表,它的长度不等于图像文件名,而是 每一个图像的每一个目标 作为列表元素,因此,该列表的长度是 数据集中全部目标的数量
        {
            "area": 2993,  // 该目标区域的面积
            "iscrowd": 0,  // 是否分块,如果是1,表示一组对象。即该目标被遮挡导致有多个小块
            "image_id": 1,  // 图片的序号id
            "bbox": [  // 该区域的mask外接矩形
                2500,
                1268,
                41,
                73
            ],
            "segmentation":[[x1,y1,x2,y2,x3,y3] ],  // 掩膜轮廓区域坐标 每相邻xy表示一个点
            "category_id": 12,  // 目标的类别序号
            "id": 1  // 目标的序号id,即在annotation类别中的序号。
        },
}

coco的json标签转xml文件

从coco转换到xml格式,只需要从json文件中提取到图像的信息,包括文件名/路径、宽高和标注信息,标注信息只需要类别id和区域的bbox。

具体代码如下,pretty_xml是用于缩进xml文件,进行美化(忘了是在哪个博客看到的了)。write函数用于写入一个xml文件,函数接受一个字典输入info,其key是图像名,value是一个列表,每个元素是图像中的一个目标,以x y w h label的顺序表示,或者x1 y1 x2 y2 label的顺序,shape参数是该图像的hwc,如果没有传入,会试图通过读取文件名来获取该信息。

写入xml的内容都必须是字符串,不能是整数或者浮点数,因此write函数中写入一些数字类型的数据,如坐标、宽高等要转成str。

import time
import cv2
import numpy as np
from PIL import Image
import json
import xml.etree.ElementTree as ET 
import random

def pretty_xml(element, indent, newline, level=0):  # elemnt为传进来的Elment类,参数indent用于缩进,newline用于换行
    if element:  # 判断element是否有子元素    
        if (element.text is None) or element.text.isspace():  # 如果element的text没有内容
            element.text = newline + indent * (level + 1)
        else:
            element.text = newline + indent * (level + 1) + element.text.strip() + newline + indent * (level + 1)
            # else:  # 此处两行如果把注释去掉,Element的text也会另起一行
            # element.text = newline + indent * (level + 1) + element.text.strip() + newline + indent * level
    temp = list(element)  # 将element转成list
    for subelement in temp:
        if temp.index(subelement) < (len(temp) - 1):  # 如果不是list的最后一个元素,说明下一个行是同级别元素的起始,缩进应一致
            subelement.tail = newline + indent * (level + 1)
        else:  # 如果是list的最后一个元素, 说明下一行是母元素的结束,缩进应该少一个    
            subelement.tail = newline + indent * level
        pretty_xml(subelement, indent, newline, level=level + 1)  # 对子元素进行递归操作



# 送入的是dict {图像名字 :框信息} 框信息是[[x1,y1,w,h,label], [x1,y1,w,h,label],[]]
def write(infos,shape=None):

    root = ET.Element('annotation',{"verified":"no"})  # 根节点

    folder = ET.SubElement(root, 'folder')
    folder.text = '1'
    img_name = list(infos.keys())[0].strip()  # 图像信息

    filename = img_name.split("/")[-1]
    file_name = ET.SubElement(root, 'filename')
    file_name.text = filename  # 文件名这个tag

    path = ET.SubElement(root, 'path')  # 路径的tag
    path.text = img_name 

    source = ET.SubElement(root, 'source')
    database = ET.SubElement(source, 'database')
    database.text = "Unknown"

    if shape:
        h,w,c = shape[0], shape[1],shape[2]
    else:
        img = cv2.imread(img_name)
        h,w,c = img.shape 
        
    size = ET.SubElement(root, 'size')
    width = ET.SubElement(size, 'width')
    width.text = str(w)
    height = ET.SubElement(size, 'height')
    height.text = str(h)
    depth = ET.SubElement(size, 'depth')
    depth.text = str(c)

    segmented = ET.SubElement(root, 'segmented')
    segmented.text = str(0)

    for key in infos.keys():
        bboxes = infos[key]
        for bbox in bboxes:
            object_tag = ET.SubElement(root, 'object')
            # bbox = ['911', '258', '153', '326', 'class1']
            name = ET.SubElement(object_tag, 'name')
            name.text = bbox[-1]  #框名字
            
            pose = ET.SubElement(object_tag, 'pose')
            pose.text = "Unspecified"
            trunc = ET.SubElement(object_tag,'truncated')
            trunc.text = '0'
            diff = ET.SubElement(object_tag,'difficult')
            diff.text = '0'
            # 下面是写入坐标框
            bndbox = ET.SubElement(object_tag,'bndbox')
            #left_x:  911   top_y:  258     153   height:  326
            bbox[2] = str(int(bbox[2]))
            bbox[3] = str(int(bbox[3]))

            xmin = ET.SubElement(bndbox,'xmin')
            xmin.text = bbox[0]

            ymin = ET.SubElement(bndbox,'ymin')
            ymin.text = bbox[1]

            xmax = ET.SubElement(bndbox,'xmax')   
            # 如果是 x y w h
            # xmax.text = str(int(bbox[0]) + int(bbox[2]) )
            # 如果是 x1 y1 x2 y2
            xmax.text = str(int(bbox[2]) )

            ymax = ET.SubElement(bndbox,'ymax')

            # ymax.text = str(int(bbox[1]) + int(bbox[3]) )
            ymax.text = str(int(bbox[3]))

    pretty_xml(root, '	', '
')  # 执行美化方法
    #ET.dump(root)
    tree = ET.ElementTree(root)
    xml_name = filename.replace("jpg","xml")  # 要保存的xml文件名字 和图像文件名相同,仅后缀不同
    tree.write(f"F:/xjzh/cocoxml/{xml_name}", encoding="utf-8",xml_declaration=False)


    

# 打开json标签文件
with open(r'F:xjzh图片annotations.json','r') as f:
    data_an = {}  # 外围大字典
    json_dicts = json.loads(f.read())  # 


cate = json_dicts['categories']
cls_name = [0]*len(cate)
for cate_dict in cate:
    cls_name[cate_dict['id']] = cate_dict['name'] # 各个类别对应的名字 按顺序放在cls_name中,如id为0的类别名就是cls_name[0]

# 类别 按照id顺序来的
#  ['背景', '瓶盖破损', '瓶盖变形', '瓶盖坏边', '瓶盖打旋', '瓶盖断点', '标贴歪斜', '标贴起皱', '标贴气泡', '喷码正常', '喷码异常', '酒液杂质', '瓶身破损', '瓶身气泡']

images_info = json_dicts["images"]  # 图像信息

image_nums = len(images_info)  # 图像数量
print(image_nums)

file_list = []  # 存放图像基本信息 依次按顺序读入,这样图像image_id是k的图像信息就是file_list[k-1]  k从1开始

annot_list = [[] for _ in range(image_nums)] # 存放每个图像对应的框,每个图像的全部目标是一个元素,因此初始化为一个长度和图像数量相同的列表,列表的每个元素是一个空列表,后面用于增加目标框信息

for index in range(image_nums):
    # 第 index张图像的信息 包括文件名 宽高
    file_name = images_info[index]["file_name"]  # id从1变换到2668  索引从0到2667
    width = images_info[index]["width"]
    height = images_info[index]['height']
    # print([file_name,width,height],'-------')
    file_list.append([file_name,width,height])  # file_list列表保存


annot = json_dicts["annotations"]
# 根据目标所属的图像id,放到列表对应的位置。从而使一张图片的多个目标聚合在一起
for index in range(len(annot)):  # 获取标签信息
    # 第index个目标的框可能是x y w h或者x1 y1 x2 y2的形式。按照实际情况修改
    annot[index]['bbox'][2] = annot[index]['bbox'][2] + annot[index]['bbox'][0]  # 这是x y w h的形式 转成x1 y1 x2 y2的形式。或者不转,和write函数保持一致就可以
    annot[index]['bbox'][3] = annot[index]['bbox'][3] + annot[index]['bbox'][1]
    
    # 这个目标的类别id是annot[index]["category_id"] 它的名字是cls[索引]  注意json文件中类别id可能是从1开始,而cls_name的索引是0开始 注意保持对应
    annot[index]['bbox'].append(cls_name[ annot[index]["category_id"] ])  # 在框信息后面 加入 label
    # print(annot[index]['bbox'],annot[index]['image_id']-1)
    # -1是因为该目标对应的图像id是从1开始,因此要放在列表的第 id -1的位置
    annot_list[annot[index]['image_id']-1].append(annot[index]['bbox'])   # bbox是 x1 y1 w h 


# 对每一张图 写入xml文件
for index in range(image_nums):
    print(index)
    box = annot_list[index]
    print(box)
    # info的key是文件名 value是坐标列表
    write({file_list[index][0]: box},shape=[str(file_list[index][1]),str(file_list[index][0]),'3'])  # shape =hwc

xml或者yolo标签转labelme的json格式

目标检测任务,而不是分割任务格式。将xml或者yolo格式的框标注文件转成json格式,格式和labelme标注结果格式相同,而不是coco数据集的json格式。

转换步骤: 从xml文件或者txt文件获取框的坐标,按照labelme的json坐标顺序组织,依次对照json的各个key填充相应内容即可。

labelme标注的json文件内容如下。

labelme 标签的json文件格式

"imagePath":"1.jpg" 图像路径

"fillColor": [255,0,0,128], # 填充颜色,RGB和透明度

"imageData":"/9j/啥啥啥" base64编码

"imageHeight": 768, 图像高度

"flags": {},

"version": "3.16.7",

"imageWidth": 1366, 图像宽度

"lineColor": [0,255,0,128], # 线条颜色

"shapes": [ # shapes是列表,每一个元素是字典。

# 每个字典格式如下

{

​ "fill_color": null,

​ "line_color": null,

​ "shape_type": "polygon", # 多边形 或者 长方形等

​ "points": [ # 列表点 xy

​ [

​ 248.8771929824561, 273.2631578947368

​ ],

​ ],

​ "flags": {},

​ "label": "person" # 标签

},

import cv2
import  xml.etree.ElementTree as ET 
import numpy as np
import os
import json
import shutil
import base64
'''
该脚本实现将xml类型标签(或者yolo格式标签)转为json格式标签
需要的数据:原始图像 原始xml标签(原始txt标签)

'''

# 解析数据集,输入单张图片路径,图片路径不能出现中文,因为是cv2读取的。和对应xml文件的路径
# 返回图片 该图所有的目标框[[x1,y1,x2,y2],....]  每个框的类别[label1, label2, label3,.....]  注意是label而不是索引
def parse_img_label(img_path, xml_path):  # 绝对路径
    img = cv2.imread(img_path)
    tree = ET.parse(xml_path) 
    root = tree.getroot()
    objs = root.findall('object')
    bboxes = []  # 坐标框
    h ,w = img.shape[0], img.shape[1]
    #gt_labels = []  # 标签名
    for obj in objs: # 遍历所有的目标
        label = obj[0].text  # <name>这个tag的值,即标签
        label = label.strip(' ')
        box = [int(obj[4][i].text) for i in range(4)]
        box.append(label)  # box的元素 x1 y1 x2 y2 类别
        bboxes.append(box)
    return img, bboxes

# 该函数用于将yolo的标签转回xml需要的标签。。即将归一化后的坐标转为原始的像素坐标
def convert_yolo_xml(box,img):  # 
    x,y,w,h = box[0], box[1], box[2], box[3]
    # 求出原始的x1 x2 y1 y2
    x2 = (2*x + w)*img.shape[1] /2
    x1 = x2 - w*img.shape[1]

    y2 = (2*y+h)*img.shape[0] /2
    y1 = y2 - h* img.shape[0]
    new_box = [x1,y1, x2, y2]
    new_box = list(map(int,new_box))
    return new_box

# 该函数用于解析yolo格式的数据集,即txt格式的标注 返回图像 边框坐标 真实标签名(不是索引,因此需要预先定义标签)
def parse_img_txt(img_path, txt_path):
    name_label = ['class0','class1','class2']  # 需要自己预先定义,它的顺序要和实际yolo格式的标签中0 1 2 3的标签对应 yolo标签的类别是索引 而不是名字
    img = cv2.imread(img_path)
    f = open(txt_path)
    bboxes = []
    for line in f.readlines():
        line = line.split(" ")
        if len(line) == 5:
            obj_label = name_label[int(line[0])] # 将类别索引转成其名字
            x = float(line[1])
            y = float(line[2])
            w = float(line[3])
            h = float(line[4])
            box = convert_yolo_xml([x,y,w,h], img)
            box.append(obj_label)
            bboxes.append(box)
    return img, bboxes



# 制作labelme格式的标签
# 参数说明 img_name: 图像文件名称 
# txt_name: 标签文件的绝对路径,注意是绝对路径
# prefix: 图像文件的上级目录名。即形如/home/xjzh/data/ 而img_name是其下的文件名,如00001.jpg
# prefix+img_name即为图像的绝对路径。不该路径能出现中文,否则cv2读取会有问题
# 
def get_json(img_name, txt_name, prefix, yolo=False):
    # 图片名 标签名 前缀
    label_dict = {}  # json字典,依次填充它的value 
    label_dict["imagePath"] = prefix + img_name  # 图片路径
    label_dict["fillColor"] = [255,0,0,128]  # 目标区域的填充颜色 RGBA
    label_dict["lineColor"] = [0,255,0,128]  # 线条颜色
    label_dict["flag"] = {}
    label_dict["version"] = "3.16.7"  # 版本号,随便
    with open(prefix + img_name,"rb") as f:
        img_data = f.read()
        base64_data = base64.b64encode(img_data)
        base64_str = str(base64_data, 'utf-8')
        label_dict["imageData"] = base64_str  # labelme的json文件存放了图像的base64编码。这样如果图像路径有问题仍然能够打开文件

    img, gt_box = parse_img_label(prefix + img_name, txt_name) if not yolo else parse_img_txt(prefix + img_name, txt_name)  # 读取真实数据
    
    label_dict["imageHeight"] = img.shape[0]  # 高度
    label_dict["imageWidth"] = img.shape[1]

    shape_list = [] # 存放标注信息的列表,它是 shapes这个键的值。里面是一个列表,每个元素又是一个字典,字典内容是该标注的类型 颜色 坐标点等等
    #label_dict["shapes"] = [] # 列表,每个元素是字典。
    # box的元素 x1 y1 x2 y2 类别
    for box in gt_box:
        shape_dict = {}  # 表示一个目标的字典
        shape_dict["shape_type"] = "rectangle"  # 因为xml或yolo格式标签是矩形框标注,因此是rectangle
        shape_dict["fill_color"] = None  #该类型的填充颜色 
        shape_dict["line_color"] = None  # 线条颜色 可以设置,或者根据标签名自己预先设定labe_color_dict
        shape_dict["flags"] = {}
        shape_dict["label"] = box[-1] # 标签名  
        shape_dict["points"] = [[box[0],box[1]], [box[2], box[3]]] 
        # 通常contours是长度为1的列表,如果有分块,可能就有多个  # [[x1,y1], [x2,y2]...]的列表
        shape_list.append(shape_dict)
    
    label_dict["shapes"] = shape_list  #
    return label_dict

imgs_path = "/home/xjzh/fgd/JPEGImages/"  # 图像路径
xmls_path ="/home/xjzh/fgd/Annotations/" # xml文件路径

img_path = os.listdir(imgs_path)
out_json = '/home/xjzh/DATA/JSON_data/'  # 保存的json文件路径

for nums, path in enumerate(img_path):
    if nums %200==0:
        print(f"processed {nums} images")
    xml_path = xmls_path + path.replace('jpg','xml')  # xml文件的绝对路径
    label_dict = get_json(path, xml_path,prefix=imgs_path)  # 
    with open(out_json + path.replace("jpg","json"),'w') as f: # 写入一个json文件
        f.write(json.dumps(label_dict, ensure_ascii=False, indent=4, separators=(',', ':')))