标注格式转换csv转xml
csv文件格式
frame removemark object_id object_id_fix id_reorder x1 y1 x2 y2 x3 y3 x4 y4 x_ori y_ori
0000001.jpg 0 0 0 0 1333 1196 1873 1162 1885 1354 1345 1388 1575 1276
0000002.jpg 0 0 0 0 1367 1207 1868 1151 1888 1335 1388 1391 1627 1271
0000002.jpg 0 1 1 1 536 1297 537 1121 972 1123 971 1299 737 1221
0000002.jpg 1 0 0 1370 1207 1867 1153 1887 1335 1390 1389 1628 1271
frame:这是最重要的列,它标识了标注所对应的图像文件名。例如,0000001.jpg。
removemark:一个标记,通常用于表示该标注是否应该被移除或忽略。0 表示保留,1 表示移除。
object_id, object_id_fix, id_reorder: 这几列通常用于跟踪和管理图像中不同对象的唯一标识符。在你的数据中,它们用于区分同一帧图像中的不同物体。例如,在 0000002.jpg 中,object_id 为 0 和 1 的两条记录代表了两个不同的物体
x1, y1, x2, y2, x3, y3, x4, y4: 这八个参数定义了旋转边界框的四个角点坐标。这些坐标通常以像素为单位,定义了图像中目标的确切位置和方向。这种格式对于标注不规则形状或倾斜的对象非常有用。
x_ori, y_ori: 这两个参数可能是原始点或中心点坐标。它们提供了额外的信息,可能用于特定的分析或处理,但对于标准的目标检测任务来说,通常只需使用 x1 到 y4 这八个参数。
xml文件格式
<annotation verified="no"><folder>camera28</folder><filename>0000001</filename><path>E:\deeplearning\torch\camera28\0000001.png</path><source><database>Unknown</database></source><size><width>2560</width><height>1440</height><depth>3</depth></size><segmented>0</segmented><object><type>robndbox</type><name>cow</name><pose>Unspecified</pose><truncated>0</truncated><difficult>0</difficult><robndbox><cx>1568.5011</cx><cy>1275.0938</cy><w>187.4719</w><h>460.3087</h><angle>1.507916</angle></robndbox></object>
</annotation>
转换脚本csv_to_xml.py
import os
import pandas as pd
import xml.etree.ElementTree as ET
from xml.etree.ElementTree import Element, SubElement
import cv2
import numpy as np
import math# --- 配置 ---
CSV_FILE = r'E:\deeplearning\torch\cow_data\40000.csv'
IMAGE_FOLDER = r'E:\deeplearning\torch\cow_data\camera28'
OUTPUT_FOLDER = r'E:\deeplearning\torch\cow_data\xml'def get_image_dimensions_and_extension(image_base_path):"""根据给定的基本路径,查找图像文件并返回其尺寸和扩展名。"""valid_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']for ext in valid_extensions:full_path = image_base_path + extif os.path.exists(full_path):img = cv2.imread(full_path)if img is not None:h, w, _ = img.shapereturn w, h, extreturn None, None, Nonedef convert_csv_to_robndbox_xml(df, output_folder, image_folder):"""将包含标注的pandas DataFrame转换为只包含robndbox的XML文件。"""if not os.path.exists(output_folder):os.makedirs(output_folder)# 按图像文件名对标注进行分组grouped_df = df.groupby('frame')# 获取图像文件夹的名称folder_name = os.path.basename(image_folder)for filename, group in grouped_df:# 尝试查找图像文件及其扩展名img_base_path = os.path.join(image_folder, os.path.splitext(filename)[0])img_w, img_h, img_ext = get_image_dimensions_and_extension(img_base_path)if img_ext is None:print(f"警告: 未找到图像文件 '{filename}',跳过。")continue# 完整的图像文件名,包含扩展名full_filename = os.path.splitext(filename)[0] + img_extannotation = Element('annotation')SubElement(annotation, 'folder').text = folder_nameSubElement(annotation, 'filename').text = full_filenameSubElement(annotation, 'path').text = os.path.join(folder_name, full_filename).replace('\\', '/')source = SubElement(annotation, 'source')SubElement(source, 'database').text = 'Unknown'size = SubElement(annotation, 'size')SubElement(size, 'width').text = str(img_w)SubElement(size, 'height').text = str(img_h)SubElement(size, 'depth').text = '3'SubElement(annotation, 'segmented').text = '0'for _, row in group.iterrows():points = [[row['x1'], row['y1']], [row['x2'], row['y2']],[row['x3'], row['y3']], [row['x4'], row['y4']]]points_np = np.array(points, dtype=np.float32)rect = cv2.minAreaRect(points_np)center, size, angle = rectw, h = sizeobject_name = 'cow'obj = SubElement(annotation, 'object')SubElement(obj, 'name').text = object_nameSubElement(obj, 'pose').text = 'Unspecified'SubElement(obj, 'truncated').text = '0'SubElement(obj, 'difficult').text = '0'SubElement(obj, 'type').text = 'robndbox'angle_rad = angle * math.pi / 180.0robndbox = SubElement(obj, 'robndbox')SubElement(robndbox, 'cx').text = f"{center[0]:.4f}"SubElement(robndbox, 'cy').text = f"{center[1]:.4f}"SubElement(robndbox, 'w').text = f"{w:.4f}"SubElement(robndbox, 'h').text = f"{h:.4f}"SubElement(robndbox, 'angle').text = f"{angle_rad:.6f}"output_path = os.path.join(output_folder, os.path.splitext(full_filename)[0] + '.xml')tree = ET.ElementTree(annotation)ET.indent(tree, space=" ", level=0)tree.write(output_path, encoding='utf-8', xml_declaration=True)print(f"成功转换 {full_filename} 为 {os.path.basename(output_path)}")# --- 主程序入口 ---
if __name__ == "__main__":try:import pandas as pdimport mathexcept ImportError:print("错误: 缺少 'pandas' 或 'math' 库。请安装。")exit()try:df = pd.read_csv(CSV_FILE)except FileNotFoundError:print(f"错误: 未找到CSV文件: {CSV_FILE}")exit()required_cols = ['frame', 'x1', 'y1', 'x2', 'y2', 'x3', 'y3', 'x4', 'y4']if not all(col in df.columns for col in required_cols):print("错误: CSV文件缺少必需的列。")exit()print(f"开始将CSV转换为XML...")convert_csv_to_robndbox_xml(df, OUTPUT_FOLDER, IMAGE_FOLDER)print("所有转换已完成。")
1、数据加载与分组
加载 CSV: 脚本首先使用 pandas.read_csv() 函数加载你的 CSV 文件。Pandas 库能将数据轻松地读入一个名为 DataFrame 的表格结构中,这比手动逐行解析纯文本文件要高效得多。
按图像分组: 你的 CSV 文件中,多行可能对应同一张图像(例如 0000002.jpg 有多条标注)。为了为每张图像生成一个独立的 XML 文件,我们使用了 df.groupby(‘frame’)。这个操作会将 DataFrame 按照 frame 列的值进行分组,每一组都包含了对应同一张图像的所有标注记录。
2、XML 文件构建循环
脚本的核心是一个 for 循环,它遍历 groupby 后的每个组(即每张图像及其所有标注)。在每次循环中,脚本都会执行以下步骤:
获取图像信息:
脚本首先尝试通过 CSV 中的文件名 (filename) 找到对应的图像文件,并使用 OpenCV (cv2.imread) 获取图像的宽度、高度和深度。这个步骤非常关键,因为 XML 文件需要这些图像元数据。
如果找不到图像文件,脚本会打印警告并跳过该帧,防止因文件缺失而导致程序崩溃。
创建 XML 骨架:
使用 ET.Element(‘annotation’) 创建 XML 文件的根元素。
接着,使用 ET.SubElement() 在根元素下添加标准的 VOC 标签,如 < folder>、< filename>、< path> 和 < size>。为了满足你对相对路径的需求,path 标签的值是通过 os.path.join(folder_name, filename) 动态生成的,确保它包含了文件夹名称和文件名,而不是绝对路径。
3、边界框数据转换与写入
这是整个脚本最复杂的部分,它在一个内层循环中处理每张图像的所有标注。
解析 CSV 行: 内层循环遍历当前图像的所有标注行。对于每一行,脚本从 x1 到 y4 列提取八个角点坐标。
坐标转换: 你的 CSV 提供了四个角点,但你需要的 XML 格式是 robndbox,它要求中心点 (cx, cy)、宽高 (w, h) 和角度 (angle)。脚本利用 OpenCV 的 cv2.minAreaRect() 函数来完成这个转换。这个函数可以根据一组点自动计算出最小外接旋转矩形的中心、尺寸和角度。minAreaRect() 返回的角度通常在 [-90, 0) 之间。脚本将这个角度直接用于 XML,这符合 VOC 格式的惯例。
构建 < object> 标签:
为每个标注创建一个 < object> 标签。在 < object> 下添加标准的子标签,如 < name>、< pose> 等。
最重要的是,添加 < type>robndbox< /type> 标签来明确指定边界框的类型。创建 < robndbox> 标签,并使用 Python 的 f-string 格式化,将转换后的 cx、cy、w、h 和 angle 值以精确的小数位数写入其中。