news 2026/10/19 14:26:15

旋转目标检测数据格式转换:roLabelImg的XML转YOLO OBB txt实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
旋转目标检测数据格式转换:roLabelImg的XML转YOLO OBB txt实战

写这篇东西的起因很简单:我手头有一批用 roLabelImg 标好的旋转目标检测数据,标注文件全是 XML。项目中途决定换到 YOLO 系列做训练,结果发现 XML 不能直接用,必须转成 txt。光看这个需求觉得不就是改个后缀吗?真动手才知道,XML 里存的是“中心点坐标 + 宽高 + 角度”,YOLO 要的是“归一化坐标 + 边界表达”,两种体系差得不是一点半点。我花了一个下午把转换脚本写完整,中间踩了不少坑。这篇文章就按我实际操作的顺序,把格式差异、转换公式、完整脚本、验证方法、常见问题全部整理出来,给同样被 roLabelImg 数据折磨的人一个直接能抄的答案。

1. 为什么不能直接把 XML 改成 txt:两种标注格式的底层差异

1.1 roLabelImg 的 XML 到底存了什么:robndbox 与 bndbox

roLabelImg 是跟着 DOTA 这类旋转目标检测任务流行起来的标注工具。它标出来的 XML 结构本身继承自 Pascal VOC,但多了旋转框专用的节点。一个普通目标的 XML 大概是这样的:

<object> <type>robndbox</type> <name>ship</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <robndbox> <cx>880.31</cx> <cy>204.87</cy> <w>106.40</w> <h>72.83</h> <angle>1.201</angle> </robndbox> </object>

关键就在<robndbox>节点里。它保存的并不是四个角点,而是一组等价参数:中心点cx、cy,矩形自身宽度w、高度h,以及旋转角度angle。这和高德地图、自动驾驶里常见的“中心点 + 长宽 + 航向角”是一个思路,存储紧凑,但模型侧并不直接吃这种表达。

需要注意,有些版本或导出配置里,目标也可能是<bndbox>普通水平框,里面是xmin, ymin, xmax, ymax。这通常意味着该目标没被旋转,或者标注时用的是水平框模式。转换脚本必须兼容这两种节点,否则遇到水平框直接跳过或者报错,数据就缺了。

除了每个<object>,XML 头部还有<size>节点,里面带了图片的width、height、depth。这个信息非常关键,后面做归一化全靠它,不要自己去猜图片尺寸,也不建议临时用 PIL 去读图。因为图片尺寸是标注时对齐的尺寸,如果原始图片被预处理过、压缩过,用当前图片尺寸去归一化,坐标比例很容易错位。

1.2 YOLO 要的 txt 是哪一种:水平框、OBB 多边形、OBB 角度格式

YOLO 官方早期版本只支持水平框,txt 每行格式是:

class_id x_center y_center width height

其中所有坐标都除以图片宽高做了归一化。如果你的目标全是水平框,那转换很简单,把bndbox或者robndbox的包围盒算出来,再输出上面格式就行。

但 roLabelImg 大量场景是旋转目标,比如遥感图像里的船、飞机、车辆。你如果强行把旋转框的外接水平矩形送进普通 YOLO,框里会混进大量背景,模型训练出来精度很难看。所以更合理的目标是旋转框检测,也就是 OBB(Oriented Bounding Box,定向边界框)。

YOLOv5 之后的部分社区版本、YOLOv8 以及 YOLO11 都支持 OBB 任务。它们的标签格式常见有两种表达:

  1. 四点多边形格式:每行是class_id x1 y1 x2 y2 x3 y3 x4 y4,全部归一化。这种格式通用性最强,很多工具、框架都能解析,也是我推荐优先转换的目标。
  2. 角度格式:每行是class_id cx cy w h angle,同样归一化中心点、宽高,角度有的用弧度、有的用度,不同框架定义不一样。这种格式需要额外确认框架要求的角度范围和旋转方向,踩坑概率更大。

既然是“roLabelImg 标定数据转 YOLO 可用 txt”,最稳妥的方案就是转成四点多边形格式。四点表达不依赖角度定义,即使个别框架角度约定不同,也可以通过四点直接恢复旋转框,兼容性最好。

2. 转换前的几个关键选择:坐标系、角度单位和类别映射

2.1 角度到底是弧度还是角度:先搞清楚再写公式

这是整个转换里最容易翻车的地方。roLabelImg 不同版本、不同配置下,XML 里的angle存在弧度制或者角度制两种可能。

常见默认情况是弧度制,所以你会看到angle数值一般是-3.14到3.14之间,或者0到3.14之间。但有些改动过的版本存的是角度制,常见数值会是类似90.0、180.0的样子。写脚本时我建议做一个自动判断,相对来说更省心。

我的判断逻辑很简单:如果angle的绝对值大于2 * math.pi,就认为它是角度制,转换时用math.radians()转成弧度;否则保持原值。这套逻辑不一定覆盖所有角落情况,但实际跑过的数据基本都能正确识别。如果不放心,就在转换后用可视化脚本抽查几张,原理上不会错。

角度方向同样要留意。图像坐标系里 y 轴向下,旋转方向的表现和数学坐标系里正好有点反直觉。公式层面我们用标准三角函数算,出来的四个点在图片上是否看着和 roLabelImg 里一致,全靠可视化验证。你不需要记太多结论,只要记住一点:比例、大小对了但方向错了,多半是宽高方向选错或者角度符号取反。

2.2 归一化到底除以谁:用 XML 里的 size 而不是猜

YOLO 的 txt 里所有坐标都必须落在[0, 1]这个区间内。也就是说,x、y要分别除以图片宽度、高度,w、h同理。

这里有个坑:很多人直接用cv2.imread()读取图片后拿到的宽高去做归一化。如果你的图片是原始标注图,那没问题;但如果图片曾经被旋转补边、裁剪缩放、甚至只是改了分辨率,再用当前图片尺寸,就会导致所有框全部漂移。

正确做法是从 XML 的<size>节点读width和height。roLabelImg 在保存标注时会把图片尺寸写进 XML,这个尺寸本身就是标注时依赖的坐标系统。如果你的数据集有数据清洗流程,也要保证清洗后的图片尺寸和 XML 里记录的一致,否则必须先对齐图片再转标签。

2.3 类别 ID 的顺序:classes.txt 是唯一的映射基准

XML 里<name>存的是类别名称,比如ship、airplane。YOLO 的 txt 第一字段是类别整数 ID,不是名称。所以必须有一个类别顺序表,让同一个名称永远对应同一个 ID。

这个顺序表就是训练时data.yaml里的names列表,或者单独维护的classes.txt。我的做法是在转换脚本顶部写死一个CLASS_NAMES列表,顺序必须和data.yaml完全一致。如果一个 XML 里出现列表之外的类别名,脚本直接抛异常而不是悄悄忽略,这样能第一时间发现标注名拼写不一致、多了空格、大小写不统一等问题。

类别映射的排查经验是:如果训练时发现某些目标类别错乱,比如船变成了飞机,十有八九是转换脚本里的CLASS_NAMES顺序和data.yaml里不一致。改之前先去看data.yaml的 names 到底是按什么顺序排的,不要凭记忆猜。

3. 核心公式:从一个旋转矩形到四个角点

3.1 旋转矩形原理和公式推导

从 roLabelImg 的robndbox拿到的是中心点(cx, cy)、宽w、高h、角度angle。要转成四点格式,本质上是把矩形数学模型展开。

旋转矩形可以理解成:先在水平状态下以原点为中心建立一个矩形,宽边沿 x 轴、高边沿 y 轴,然后绕中心旋转角度angle,最后平移到(cx, cy)。根据这个思路,宽边方向的单位向量是:

(cos(angle), sin(angle))

高边方向的单位向量是:

(-sin(angle), cos(angle))

于是,宽边偏移向量是(w/2 * cos(angle), w/2 * sin(angle)),高边偏移向量是(-h/2 * sin(angle), h/2 * cos(angle))。

四个角点可以这样算:

  • p1 = (cx - 宽偏移 + 高偏移)?不对,需要统一顺时针顺序。

更清楚一点,设宽边偏移为v_w,高边偏移为v_h,那么从中心出发:

p1 = 中心 - v_w - v_h p2 = 中心 + v_w - v_h p3 = 中心 + v_w + v_h p4 = 中心 - v_w + v_h

这样得到的四个点,顺序是围绕中心顺时针排列的。只要角点顺序稳定,YOLO OBB 训练时就能正确还原矩形。

公式里的angle是弧度制。如果你处理的是角度制,先转换再代入。图像坐标系 y 向下并不影响这个公式的代数推导,只是画出来时视觉上顺逆时针会发生变化,实际验证一下就好。

3.2 手工验算一遍:把数字代入看结果

为了确认公式没写错,我习惯先手工算一个简单例子。

假设图片宽度 1000,高度 800。某个目标:

cx = 300 cy = 200 w = 100 h = 50 angle = 0

当角度为 0 时,宽边水平方向,高边垂直方向,所以两个偏移向量分别是:

v_w = (50, 0) v_h = (0, 25)

四个角点就变成:

p1 = (300 - 50 - 0, 200 - 0 - 25) = (250, 175) p2 = (300 + 50 - 0, 200 + 0 - 25) = (350, 175) p3 = (300 + 50 + 0, 200 + 0 + 25) = (350, 225) p4 = (300 - 50 + 0, 200 - 0 + 25) = (250, 225)

这不就是一个水平矩形嘛,和实际完全吻合。再把角度改成math.pi / 2,也就是 90 度,此时cos=0, sin=1:

v_w = (0, 50) v_h = (-25, 0)

角点就变成:

p1 = (300 + 50, 200 + 25)? 实际算: p1 = (300 - 0 - (-25), 200 - 50 - 0) = (325, 150) p2 = (300 + 0 - (-25), 200 + 50 - 0) = (325, 250) ...

四个点整体变成竖直的长矩形,和 90 度旋转后的感觉一致。手工验算能让我清楚地确认公式的方向逻辑,也方便排查脚本里的符号错误。

4. 完整转换脚本:roLabelImg XML 转 YOLO OBB txt

4.1 完整 Python 脚本(可跑)

下面这个脚本是我实际在用的一个版本,支持robndbox和bndbox两种节点,自动判断弧度或角度,输出 YOLO OBB 四点格式。你可以直接复制,改一下路径和类别列表就能跑。

import os import math import glob import xml.etree.ElementTree as ET # 类别顺序,必须和训练时 data.yaml 里的 names 保持一致 CLASS_NAMES = ["airplane", "ship", "vehicle"] def parse_xml(xml_path): """解析 roLabelImg 生成的 XML,返回图片宽高和所有目标框。""" tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) boxes = [] for obj in root.findall("object"): name = obj.find("name").text.strip() difficult = 0 ele_diff = obj.find("difficult") if ele_diff is not None and ele_diff.text: difficult = int(ele_diff.text) obj_type = "robndbox" ele_type = obj.find("type") if ele_type is not None and ele_type.text: obj_type = ele_type.text.strip() if obj_type == "robndbox": rb = obj.find("robndbox") cx = float(rb.find("cx").text) cy = float(rb.find("cy").text) w = float(rb.find("w").text) h = float(rb.find("h").text) angle = float(rb.find("angle").text) # 自动判断:如果绝对值很大,大概率是角度制,转成弧度 if abs(angle) > 2 * math.pi: angle = math.radians(angle) boxes.append((name, cx, cy, w, h, angle)) else: # 普通水平框 bndbox bb = obj.find("bndbox") xmin = float(bb.find("xmin").text) ymin = float(bb.find("ymin").text) xmax = float(bb.find("xmax").text) ymax = float(bb.find("ymax").text) cx = (xmin + xmax) / 2.0 cy = (ymin + ymax) / 2.0 w = xmax - xmin h = ymax - ymin boxes.append((name, cx, cy, w, h, 0.0)) return img_w, img_h, boxes def get_corners(cx, cy, w, h, angle): """根据中心点、宽高、弧度角求矩形四个角点。""" cos_a = math.cos(angle) sin_a = math.sin(angle) vw_x = w / 2.0 * cos_a vw_y = w / 2.0 * sin_a vh_x = -h / 2.0 * sin_a vh_y = h / 2.0 * cos_a p1 = (cx - vw_x - vh_x, cy - vw_y - vh_y) p2 = (cx + vw_x - vh_x, cy + vw_y - vh_y) p3 = (cx + vw_x + vh_x, cy + vw_y + vh_y) p4 = (cx - vw_x + vh_x, cy - vw_y + vh_y) return [p1, p2, p3, p4] def convert_xml_to_txt(xml_path, out_dir, class_names): img_w, img_h, boxes = parse_xml(xml_path) img_name = os.path.splitext(os.path.basename(xml_path))[0] lines = [] for name, cx, cy, w, h, angle in boxes: if name not in class_names: raise ValueError(f"{xml_path} 里发现未定义类别:{name}") cls_id = class_names.index(name) corners = get_corners(cx, cy, w, h, angle) norm_pts = [] for px, py in corners: norm_pts.append(px / img_w) norm_pts.append(py / img_h) line_parts = [str(cls_id)] + [f"{v:.6f}" for v in norm_pts] lines.append(" ".join(line_parts)) os.makedirs(out_dir, exist_ok=True) out_path = os.path.join(out_dir, img_name + ".txt") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) return out_path # 批量转换 if __name__ == "__main__": xml_dir = "your_xml_dir" # 改成你的 XML 目录 txt_dir = "your_txt_dir" # 改成你的输出目录 for xml_path in glob.glob(os.path.join(xml_dir, "*.xml")): out = convert_xml_to_txt(xml_path, txt_dir, CLASS_NAMES) print("已生成:", out)

这段脚本的逻辑很直接:先解析 XML 拿到图片尺寸和目标框,再根据角点公式算出四个点,最后归一化写入 txt。如果遇到未定义类别,直接抛异常,这是有意为之,好让我们在数据准备阶段就发现问题。

4.2 效果验证:用 OpenCV 画框看转换是否成功

脚本能跑通不等于转换正确。坐标运算里一旦角度方向或者宽高对应关系写错,程序不会报错,但转出来的框和原图根本对不上。所以转换完一定要可视化抽查。

我写了一个快速验证脚本,读取 txt 中的归一化四点,乘回图片宽高后画在原图上:

import cv2 import numpy as np def draw_txt_on_image(image_path, txt_path): img = cv2.imread(image_path) h, w = img.shape[:2] with open(txt_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() cls_id = int(parts[0]) coords = [float(x) for x in parts[1:]] pts = np.array(coords, dtype=np.float64).reshape(-1, 2) pts[:, 0] *= w pts[:, 1] *= h pts = np.int0(pts) cv2.drawContours(img, [pts], -1, (0, 0, 255), 2) cv2.putText(img, CLASS_NAMES[cls_id], tuple(pts[0]), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2) return img # 示例 # img = draw_txt_on_image("your_image.jpg", "your_txt_dir/your_image.txt") # cv2.imwrite("check.jpg", img)

验证时重点看三件事:

  • 框是不是刚好包住目标,有没有明显偏移。
  • 框的朝向和目标是否一致,如果整体转了 90 度,说明宽高长边方向理解反了。
  • 框有没有超出图片区域,如果轻微越界是标注本身的问题,如果大面积越界,多半是归一化除错了尺寸。

我用这个方法抽查过几百张图,确认没问题之后才拿去训练,后面模型收敛也正常。

5. 不同框架下的格式变形:普通 YOLO、OBB 角度格式、分割格式

5.1 转普通水平检测框

如果你的任务并不需要旋转框,只想用普通 YOLO 训练水平目标检测,那只要把旋转框的外接水平矩形算出来就行。可以用cv2.minAreaRect的四个角点取min和max,也可以直接从get_corners得到的四个点里求包围盒。

输出格式为:

class_id x_center_norm y_center_norm width_norm height_norm

其中x_center_norm = (xmin + xmax) / 2 / img_w,同理y_center_norm,width_norm = (xmax - xmin) / img_w。

这里有一个取舍:外接水平矩形如果目标本身旋转角度大,框里会包含大量背景,容易被别的目标干扰。所以即便只是做水平检测,也建议先看看数据里旋转比例,再决定是否真要放弃角度信息。

5.2 转带角度的 OBB 标签(cx cy w h angle)

如果你用的框架明确要求 OBB 角度格式,比如某些基于 YOLOv5 改来的旋转框分支,那么脚本可以改成直接输出:

class_id cx_norm cy_norm w_norm h_norm angle

其中cx_norm = cx / img_w,cy_norm = cy / img_h,w_norm = w / img_w,h_norm = h / img_h,角度保留弧度值,或者按框架要求转成角度制。

使用角度格式时,最麻烦的是角度范围约定。有些框架定义角度为长边与 x 轴的夹角,范围在[-90, 90)度;有些框架定义成[0, 180);还有框架要求角度必须是负数。我建议先去看框架源码里解码标签的部分,确认它期望的角度定义,再在脚本里做一步适配。没有统一标准前,直接套用很容易练着练着就 Loss 爆掉。

相比之下,四点格式绕开了角度原始定义,所以我在默认方案里都优先转四点格式。

5.3 类别数量不一致 / 多级目录处理

实际项目往往不是干净的一个目录。XML 散在多个子文件夹里,图片和标注可能还分训练、验证、测试三个目录。批量脚本最好支持遍历子目录:

from pathlib import Path xml_root = Path("your_xml_root") for xml_path in xml_root.rglob("*.xml"): rel = xml_path.relative_to(xml_root) out_dir = Path("your_txt_root") / rel.parent convert_xml_to_txt(str(xml_path), str(out_dir), CLASS_NAMES)

rglob会递归找到所有 XML,同时保持目录结构,输出 txt 时也放到对应子目录里,后面做训练集验证集划分比较方便。类别数量不一样也很常见,比如新数据加了一个新类别,转换成 txt 之前,先把CLASS_NAMES更新并同步修改data.yaml。否则脚本抛异常或者类别 ID 错位,训练出来就是一团浆糊。

6. 常见问题与排查技巧实录

6.1 问题速查表

现象可能原因处理方法
转换出来的 txt 是空文件XML 里没有<object>,或者解析时被跳过检查 XML 内容和脚本里的obj_type判断
类别 ID 全部是 0脚本里的类别列表顺序和实际数据不对打印name和cls_id核对
框整体偏移归一化时用了错误的图片尺寸用 XML 里<size>的宽高,别用图片当前尺寸
框朝向相反角度符号取反或宽高向量方向不对可视化后手动调整get_corners里的+ / -符号
框变成旋转 90 度长边对应的宽度计算反了把w和h互换再转一次
转换抛ValueError数据里有未定义类别名把新类别加进CLASS_NAMES
txt 坐标出现负数或大于 1原标注点落在图片边缘外视情况做clip(0, 1),但不能解决大范围越界
中文类别名乱码XML 编码或终端编码问题打开 XML 看头部 encoding;脚本用 utf-8 读写

这张表是我自己踩坑后整理出来的。里面最坑的一条就是“类别 ID 全部是 0”,我第一次遇到时还以为是脚本逻辑写错了,最后发现是新旧两个项目的类别顺序不一致,导致所有框都被映射成第一个类别。

6.2 排查技巧与我的经验

先说一个大原则:能不写复杂逻辑就不写,尽量保持转换脚本朴素可读。格式转换本质是一个确定性过程,越花哨越容易出错,调试成本也越高。

具体排查时,我会先拿单张图打通全流程,再批量处理。单张图跑的流程是:XML 转 txt,txt 可视化画框,和 roLabelImg 里原标注对比。只要单张图验证没问题,批量脚本一般不会蹦出格式问题。

还有一个实用小技巧:转换完成后,随机挑 20 个 txt 文件检查行数,再和对应 XML 里的<object>数量对比。如果某个文件少了目标,那多半是脚本把某种节点类型漏掉了。用下面这句可以快速看:

for f in your_txt_dir/*.txt; do echo "$f $(wc -l < $f)"; done

另外,不要忽略 XML 里的<difficult>字段。有些数据把难例标成difficult=1。如果训练时不想包含这些样本,转换脚本里可以增加条件过滤。但过滤前先确认你的任务定义,很多比赛数据集里难例也是训练标签的一部分,过滤错了反而降低数据量。

最后再分享一个我后来一直在用的习惯:转换脚本不要用完了就删,把它放在数据集目录下,并且写上固定版本。因为数据集会迭代,新标注数据进来可能生成新的 XML 格式,只要有脚本,重跑一次就完事,还能横向对比不同批次的数据是否都转对了。如果没有脚本,就只能手动处理,一旦数据量大,那就真的会让人崩溃。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 14:25:19

FPGA实现VGA显示系统:从时序控制到EDID读取的完整工程实践

简介&#xff1a;本资源是一套面向FPGA初学者与嵌入式硬件开发者的VGA显示系统实践资料包&#xff0c;聚焦视频输出、外设通信与存储扩展三大核心能力培养&#xff0c;适用于课程设计、毕业设计及小型图像处理项目开发。包内涵盖VGA时序驱动代码&#xff08;含乒乓球游戏实例&a…

作者头像 李华
网站建设 2026/10/19 14:24:49

CMake execute_process 指令深度解析:构建时系统交互核心机制

1. 项目概述&#xff1a;为什么一个看似“冷门”的指令值得单独写三千字&#xff1f; execute_process 这个指令在 CMake 官方文档里只有一页半的说明&#xff0c;连示例都只给了三个最基础的用法。我第一次在某开源项目的 CMakeLists.txt 里看到它时&#xff0c;还以为是自…

作者头像 李华
网站建设 2026/10/19 14:23:08

从零搭建可用的skills技能管理体系:分类、评估与动态维护实操指南

1. 从“skills”这个词说起&#xff1a;为什么它突然成了高频搜索词最近一段时间&#xff0c;“skills”这个词在各类社区和搜索榜单上出现的频率明显高了起来。不管是在职场交流群、技术社区&#xff0c;还是各种学习型社群里&#xff0c;大家都在讨论“skills”相关的话题。有…

作者头像 李华
网站建设 2026/10/19 14:22:25

树莓派5上的YOLOv5部署:ONNX Runtime推理优化实践

把YOLOv5模型从训练环境搬到树莓派5上跑目标检测&#xff0c;最直接的办法当然是在树莓派上装PyTorch&#xff0c;然后老老实实跑detect.py。但真走到部署这步&#xff0c;你会发现这条路在ARM开发板上走得太不顺&#xff1a;依赖装起来费劲&#xff0c;推理慢&#xff0c;内存…

作者头像 李华
网站建设 2026/10/19 14:20:35

开源CLI代码评审工具open-code-review深度解析

1. 项目概述&#xff1a;这不是又一个“AI写代码”玩具&#xff0c;而是一套可嵌入日常开发流的开源代码评审协作者“open-code-review”这个名字乍听平平无奇&#xff0c;但拆开来看——open&#xff08;开源&#xff09;、code&#xff08;代码&#xff09;、review&#xff…

作者头像 李华
网站建设 2026/10/19 14:19:38

冒泡排序深度解析:原理、优化与工程实践完整指南

“请你当场写一段冒泡排序。”——这是我做技术面试时经常问的第一道算法题。让我意外的是&#xff0c;真正能在纸上把冒泡排序写对、写清楚的人&#xff0c;比想象中少得多。不少人一上来就写两层for循环&#xff0c;第二层循环的结束条件随手写个arr.length&#xff0c;或者把…

作者头像 李华