标注格式转换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文件格式

python 复制代码
<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

python 复制代码
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\55500.csv'
IMAGE_FOLDER = r'E:\deeplearning\torch\cow_data\frames_full'
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 + ext
        if os.path.exists(full_path):
            img = cv2.imread(full_path)
            if img is not None:
                h, w, _ = img.shape
                return w, h, ext
    return None, None, None


def convert_csv_to_robndbox_xml(df, output_folder, image_folder):
    """
    将包含标注的pandas DataFrame转换为只包含robndbox的XML文件。
    该脚本会过滤掉 removemark = 1 的标注。
    并且将 object_id 作为 <name> 标签的值。
    """
    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_ext

        annotation = Element('annotation')
        SubElement(annotation, 'folder').text = folder_name
        SubElement(annotation, 'filename').text = full_filename
        SubElement(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():
            # 过滤 removemark = 1 的行
            if int(row['removemark']) == 1:
                print(f"跳过 '{full_filename}' 中 removemark=1 的标注.")
                continue

            # --- 核心修改:使用 object_id 作为 name ---
            object_id = row['object_id']
            # 将 object_id 转换为字符串,通常在分类任务中需要将类别名作为字符串
            # 比如 'cow_0', 'cow_1' 等,这里直接使用ID
            object_name = str(object_id)

            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 = rect

            w, h = size

            obj = SubElement(annotation, 'object')
            SubElement(obj, 'name').text = object_name
            SubElement(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.0
            robndbox = 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 pd
        import math
    except 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', 'removemark', 'object_id']
    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 值以精确的小数位数写入其中。

相关推荐
sun00770014 分钟前
网络配置config.xml的android.mk解析
android·xml
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ8 小时前
mapper.xml中limit分页参数不能使用计算表达式问题
xml
余防1 天前
XXE - 实体注入(xml外部实体注入)
xml·前端·安全·web安全·html
未来之窗软件服务1 天前
万象EXCEL开发(四)格式解读theme1.xml ——东方仙盟练气期
xml·仙盟创梦ide·东方仙盟·万象excel
未来之窗软件服务2 天前
万象EXCEL开发(二)格式解读sharedStrings.xml——东方仙盟练气期
xml·excel·仙盟创梦ide·东方仙盟·万象excel·东方仙盟格式
goTsHgo3 天前
Spring XML 配置简介
xml·java·spring
半导体守望者3 天前
TR帝尔编码器GSD文件 PROFIBUS XML PROFINET EtherCAT 文件 ADH CDH CMV等
xml·经验分享·笔记·机器人·自动化·制造
极光雨雨3 天前
XML中的 CDATA mybaitis xml中的 <![CDATA[ xxxx ]]>
xml
未来之窗软件服务4 天前
万象EXCEL开发(三)格式解读calcChain.xml——东方仙盟练气期
xml·仙盟创梦ide·东方仙盟·万象excel
MintYouth4 天前
【精】C# 精确判断XML是否存在子节点
xml·c#