目标检测数据集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 width: 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, '\t', '\n') # 执行美化方法
#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=(',', ':')))