恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
rolabelimg XML转YOLOv8 OBB TXT:旋转目标检测标签格式转换实战
首页
资讯中心
/
rolabelimg XML转YOLOv8 OBB TXT:旋转目标检测标签格式转换实战
rolabelimg XML转YOLOv8 OBB TXT:旋转目标检测标签格式转换实战
发布时间:2026/10/19 17:35:02
做旋转目标检测的都知道标注完一批数据只是万里长征第一步最磨人的反而是格式转换。rolabelimg保存的是Pascal VOC风格的XML里面还夹带一个robndbox旋转框节点而YOLO系训练要的是归一化的TXT如果跑OBB还得是四点坐标。这个“xml转txt”的需求我前前后后处理过好多次今天把完整的踩坑过程和可直接抄的脚本整理出来希望能帮正在做遥感、航拍、版面检测这类旋转目标的同学省下半天时间。这事适合谁呢一句话只要你的数据是用rolabelimg标的而训练打算用YOLOv8 OBB或者YOLOv5 OBB这类模型那这篇就是为你写的。如果你标的是普通水平框逻辑也完全兼容可以顺手转成HBB格式。1. 为什么非要绕一圈从rolabelimg的XML到YOLO的TXT1.1 这个格式问题到底出在哪rolabelimg本身就是基于labelImg改造的标注工具所以它的输出带有明显的VOC血统一个XML文件对应一张图片里面用object节点存放检测目标每个目标下面除了常规的bndbox外接水平矩形之外还额外存了一个robndbox节点里面是旋转框的中心点、宽、高和角度。问题在于YOLO训练器根本不认XML它只认和图片同名的txt文件每行一个目标类别加坐标全归一化。于是这两个格式之间横着一条鸿沟XML是嵌套结构、绝对坐标、保留了旋转角度txt是平铺结构、归一化坐标、只认点坐标。这个矛盾刚接触的人容易低估觉得“不就是把cx、cy、w、h除以图像宽高嘛”。但旋转框和水平框不一样旋转框多了角度而且还得分清角度定义是哪套坐标系宽和高谁对应角度那根轴四角点顺序有没有要求。任何一个细节错了训练时loss直接起飞可视化时框歪得离谱。1.2 旋转框检测为什么离不开这个转换有人会问我标注的明明是一个斜着的飞机直接用一个水平矩形包起来不行吗不行。遥感影像里的目标动不动就是几十上百个密集排列比如港口里并排停靠的船只、停车场里的车辆、农田里的建筑物如果用水平外接框两个相邻目标的框会大面积重叠IoU计算失真NMS会把原本独立的目标合并掉。所以旋转检测框OBB在这些场景几乎是必须的。而YOLO系列从v5开始引入OBB支持v8做得更成熟训练时要求标签为class x1 y1 x2 y2 x3 y3 x4 y4。注意这里不是中心点加宽高加角度而是四个角点的归一化坐标。因为角度回归存在周期性问题0度和360度在数值上差很远但几何上是同一个框模型很难学四点坐标回归相对更温和。所以我们把rolabelimg的XML转成txt本质上是把“中心点宽高角度”这种用角度表达的旋转矩形换算成“四个点坐标”的表达。1.3 转换的本质一个坐标系到另一个坐标系这个转换看起来就是套公式但背后是两套完全不同的坐标约定。rolabelimg里的坐标是基于图像像素坐标系原点在左上角x向右y向下单位是像素YOLO的标签是归一化坐标系同样原点在左上角但坐标范围在0到1之间跟图像分辨率无关。中间还涉及一个细节rolabelimg中的robndbox用的是旋转矩形五参数学表示而YOLO要的是多边形四角点表示。因此转换任务拆开就是三步第一步从XML里解析出五参数第二步根据角度定义计算旋转矩形的四个顶点像素坐标第三步把四个点分别除以图像宽高做归一化类别名换成类别ID按YOLO的格式写进txt。理清楚这个流程后后面所有问题都是围绕这三个环节展开的。2. 动手前必知两种格式的字段与坐标系约定2.1 先看懂rolabelimg的XML以实际标注结果为例一个典型的rolabelimg XML长这样annotation folderimages/folder filenameimg_001.jpg/filename size width1024/width height768/height depth3/depth /size object nameship/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin300/xmin ymin200/ymin xmax700/xmax ymax500/ymax /bndbox robndbox cx500/cx cy350/cy w600/w h200/h angle-26.57/angle /robndbox /object /annotation关键节点我拆开讲filename是图片文件名转换时先按它找图找不到就用XML同名文件兜底。size里的width和height是归一化必须的除数注意深度字段depth容易被漏读但转格式时用不到。object下可能有多个目标每个object代表一个框。转换脚本要遍历所有object。bndbox是水平外接框四角点。它存在的意义是兼容普通目标检测对于旋转框转换我们主要看robndbox。robndbox里的cx、cy是旋转框中心点w是角度对应的那条边的长度h是另一条边的长度angle是旋转角度。这是命根子也是最容易出错的地方。有一个细节值得提醒不同小版本的rolabelimg保存的XML结构不完全一样。有的版本把angle写在robndbox里有的版本是往bndbox里加了angle属性还有的版本robndbox的w和h顺序不同。建议拿到数据先随机打开三五个XML肉眼确认节点结构再写脚本。别问我为什么强调这个我吃过亏写好的脚本一跑就挂结果是对方给的标注工具版本比较老。2.2 再看懂YOLO的TXTHBB和OBB的区别普通YOLO水平框的txt格式是class cx cy w h其中cx、cy是中心点坐标w、h是框宽高全部归一化到0到1。这是HBB格式只能表达轴对齐矩形。YOLOv8 OBB的txt格式是class x1 y1 x2 y2 x3 y3 x4 y4class是类别ID后面四组坐标是旋转矩形的四个顶点同样全部归一化。注意这一行里没有任何角度字段模型靠四个点来隐式表达旋转。所以转换时我们必须把cx、cy、w、h、angle这个五参数表示换算成四角点。一个常见误区是有人直接把cx、cy、w、h归一化后当成OBB的“伪四点”用只把水平外接框的四个角写进去。这样角度信息完全丢了复现出来的框是水平的旋转检测直接变水平检测之前标注的旋转角度白费。转换时必须用robndbox不能用bndbox做替代。2.3 角度定义是最大的坑OpenCV角度与DOTA角度robndbox里的angle到底是什么定义这一点不搞清楚所有的公式都是空中楼阁。rolabelimg底层计算旋转框时用的是OpenCV那套约定角度范围是[-90, 0)单位是度表示的是矩形第一条边也就是w对应的边和图像x轴正方向之间的夹角因为图像坐标系y轴向下负角在视觉上表现为顺时针旋转。举例来说一个angle-30度的矩形它的长边相对于水平方向顺时针转了30度。这个角度定义跟数学教材里的逆时针为正刚好相反所以直接用math库计算时要特别当心。另一套常见定义是DOTA数据集的约定角度范围通常是0到180度逆时针为正两个角点坐标的排列方式也不一样。很多网上找的转换脚本是从DOTA工具改的不检查就直接套用转出来的框要么镜像要么顶点顺序完全反掉。所以写转换脚本前先回答一个问题你的angle是OpenCV定义的负值锐角还是DOTA定义的正值角如果是rolabelimg默认导出的按OpenCV定义处理即可。2.4 输出标签的目录结构YOLO训练时对数据集目录有约定虽然不是强制的但最好按标准来dataset/ ├── images/ │ ├── train/ │ │ ├── img_001.jpg │ │ └── ... │ └── val/ ├── labels/ │ ├── train/ │ │ ├── img_001.txt │ │ └── ... │ └── val/ ├── classes.txt每个txt必须和对应图片同名同目录层级后缀从.jpg改成.txt。图片在images/train里标签就要在labels/train里。classes.txt一行一个类别名顺序就是类别ID顺序不能随便乱排不然后面训练出来的模型类别错位。我在转换脚本里会直接把classes.txt生成出来并提示你检查一遍顺序。因为如果你的原数据里有ship、plane、vehicle三个类脚本按字母序排出来的是plane、ship、vehicleID分别对应0、1、2。如果你之前训练过一版按别的顺序排列的就得保持旧的classes.txt顺序否则新旧标签ID对不上模型直接废掉。3. 核心转换原理从五参数到四角点的完整推导3.1 五参数怎么算四点公式与来历从五参数(cx, cy, w, h, angle)计算四个顶点的数学原理不复杂本质上就是先把矩形在局部坐标系里定义然后旋转平移到图像坐标系。先说局部坐标系。假设矩形中心在原点w边沿x轴方向h边沿y轴方向不考虑旋转时四个顶点是(-w/2, -h/2)、(w/2, -h/2)、(w/2, h/2)、(-w/2, h/2)。现在把矩形旋转一个角度也就是把所有顶点乘一个旋转矩阵。图像坐标系下角度为angle这里angle是度先转成弧度theta旋转矩阵是| cos(theta) sin(theta) | | -sin(theta) cos(theta) |等等这里要小心。上面这个旋转矩阵适用于数学坐标系逆时针旋转。但在图像坐标系里rolabelimg的angle是负角度同时y轴向下直接用下面这个公式组套是经过很多人验证的import math def rbox_to_4points(cx, cy, w, h, angle): theta math.radians(angle) cos_t math.cos(theta) sin_t math.sin(theta) x1 cx - w / 2 * cos_t h / 2 * sin_t y1 cy - w / 2 * sin_t - h / 2 * cos_t x2 cx w / 2 * cos_t h / 2 * sin_t y2 cy w / 2 * sin_t - h / 2 * cos_t x3 cx w / 2 * cos_t - h / 2 * sin_t y3 cy w / 2 * sin_t h / 2 * cos_t x4 cx - w / 2 * cos_t - h / 2 * sin_t y4 cy - w / 2 * sin_t h / 2 * cos_t return [(x1, y1), (x2, y2), (x3, y3), (x4, y4)]如果你懒得记公式手上有OpenCV的话更稳妥的做法是直接用cv2.boxPointsimport numpy as np import cv2 def rbox_to_4points_cv(cx, cy, w, h, angle): box ((cx, cy), (w, h), angle) # OpenCV角度单位度 points cv2.boxPoints(box) # 返回4个顶点 return points.tolist()两种方法结果一致。cv2.boxPoints的好处是它内部就是OpenCV同一套角度约定只要angle是rolabelimg导出的原始角度基本不会出错。建议你在小样本数据上两套方法都跑一遍对比结果确认公式理解无误后再批量跑。3.2 归一化处理和类别映射计算出四个顶点的像素坐标后归一化就是把每个坐标除以图像的宽或高norm_points [(x / img_w, y / img_h) for x, y in points]这里有个容易忽略的细节图像宽度对应所有x坐标的除数高度对应所有y坐标的除数不是用对角线的长度统一除。有些半懂不懂的代码会把四个点用最大坐标值归一化这是错的。类别映射则分两种情况如果整个数据集是第一次转换就直接收集所有XML里的name去重后按字母序排序写入classes.txt。后续转换时每个name去classes.txt里查它的下标就是类别ID。如果以前已经有classes.txt那务必以旧的为准不要重新排序。建议在脚本里加一个参数要么自动生成classes要么指定已有classes文件。还有一个细节同一张图里的同名目标非常多转换脚本里每张图写完txt后要检查一下如果某个XML里没有object那就生成一个空txt。很多YOLO训练流程允许空标签文件存在但没有对应txt会报错所以空文件也要生成。3.3 完整转换脚本可以直接用下面给出一个完整可运行的脚本。我用的是标准库加少量依赖核心转换部分只依赖math可视化部分需要opencv-python如果你不需要可视化可以把那段注释掉。import os import math import xml.etree.ElementTree as ET from collections import defaultdict # ---------- 配置 ---------- XML_DIR path/to/xmls # XML文件夹 IMG_ROOT path/to/images # 图片根目录 LABEL_ROOT path/to/labels # 输出标签根目录 CLASSES_FILE path/to/classes.txt # 类别文件可自动生成 USE_OLD_CLASSES False # 是否使用已有的classes.txt # ------------------------- def get_all_xml_files(xml_dir): return [os.path.join(xml_dir, f) for f in os.listdir(xml_dir) if f.endswith(.xml)] def collect_classes(xml_files): class_set set() for xml_file in xml_files: tree ET.parse(xml_file) root tree.getroot() for obj in root.findall(object): name obj.findtext(name).strip() class_set.add(name) return sorted(class_set) def load_existing_classes(classes_file): classes [] with open(classes_file, r, encodingutf-8) as f: for line in f: line line.strip() if line: classes.append(line) return classes def rbox_to_4points(cx, cy, w, h, angle): theta math.radians(angle) cos_t math.cos(theta) sin_t math.sin(theta) x1 cx - w / 2 * cos_t h / 2 * sin_t y1 cy - w / 2 * sin_t - h / 2 * cos_t x2 cx w / 2 * cos_t h / 2 * sin_t y2 cy w / 2 * sin_t - h / 2 * cos_t x3 cx w / 2 * cos_t - h / 2 * sin_t y3 cy w / 2 * sin_t h / 2 * cos_t x4 cx - w / 2 * cos_t - h / 2 * sin_t y4 cy - w / 2 * sin_t h / 2 * cos_t return [(x1, y1), (x2, y2), (x3, y3), (x4, y4)] def convert_one_xml(xml_file, img_root, label_root, class2id, img_w, img_h): # 这里img_w和img_h从XML的size节点读避免依赖图片本身 tree ET.parse(xml_file) root tree.getroot() filename root.findtext(filename).strip() if not filename: basename os.path.basename(xml_file) filename os.path.splitext(basename)[0] .jpg stem os.path.splitext(filename)[0] txt_path os.path.join(label_root, stem .txt) lines [] for obj in root.findall(object): name obj.findtext(name).strip() if name not in class2id: print(f警告: {xml_file} 中发现未注册类别 {name}跳过该目标) continue cls_id class2id[name] # 优先用robndbox没有则退回bndbox水平框 robndbox obj.find(robndbox) if robndbox is not None: cx float(robndbox.findtext(cx)) cy float(robndbox.findtext(cy)) w float(robndbox.findtext(w)) h float(robndbox.findtext(h)) angle float(robndbox.findtext(angle)) points rbox_to_4points(cx, cy, w, h, angle) else: bndbox obj.find(bndbox) if bndbox is None: print(f警告: {xml_file} 的object缺少bndbox和robndbox跳过) continue xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) cx (xmin xmax) / 2 cy (ymin ymax) / 2 w xmax - xmin h ymax - ymin angle 0.0 points rbox_to_4points(cx, cy, w, h, angle) norm_points [] for x, y in points: nx x / img_w ny y / img_h norm_points.append((nx, ny)) line str(cls_id) for nx, ny in norm_points: line f {nx:.6f} {ny:.6f} lines.append(line) with open(txt_path, w, encodingutf-8) as f: if lines: f.write(\n.join(lines) \n) else: f.write() # 生成空文件保持标签文件存在 def main(): os.makedirs(LABEL_ROOT, exist_okTrue) xml_files get_all_xml_files(XML_DIR) print(f找到 {len(xml_files)} 个XML文件) if USE_OLD_CLASSES and os.path.exists(CLASSES_FILE): classes load_existing_classes(CLASSES_FILE) else: classes collect_classes(xml_files) if not classes: print(没有发现任何类别检查XML文件内容) return with open(CLASSES_FILE, w, encodingutf-8) as f: for c in classes: f.write(c \n) class2id {c: i for i, c in enumerate(classes)} print(f类别映射: {class2id}) for xml_file in xml_files: tree ET.parse(xml_file) root tree.getroot() size root.find(size) img_w float(size.findtext(width)) img_h float(size.findtext(height)) convert_one_xml(xml_file, IMG_ROOT, LABEL_ROOT, class2id, img_w, img_h) print(转换完成) if __name__ __main__: main()这个脚本我在实际数据上跑过生成的txt可以直接喂给YOLOv8 OBB训练。有几个设计上的考量说一下图片宽高优先从XML的size节点读取不额外打开图片文件。这是因为大批量转换时反复读图很费时间而XML里的size只要标注工具没写错就是准的。如果你不放心可以打开一张图核对一下XML里的宽高和真实图是否一致。每个XML的目标循环里优先取robndbox取不到就退回bndbox。这样即使你的数据里有少量没标旋转角度的框脚本也能兜底不至于整个XML报错中断。输出txt时统一保留6位小数。这个精度对归一化坐标来说足够不需要保留一大串。空标签也生成文件避免训练时报“图片没有对应标签文件”的错。3.4 参数说明与运行方式使用前把XML_DIR改成放XML的目录IMG_ROOT如果只是占位可以不改脚本没有真正使用它。LABEL_ROOT是输出目录脚本会自动创建。CLASSES_FILE是类别文件的路径第一次运行会生成之后如果要用旧的就设USE_OLD_CLASSES为True。运行方式很简单python xml2yolo_obb.py运行结束后到LABEL_ROOT随便打开几个txt应该能看到类似下面的内容2 0.3024 0.2548 0.4320 0.1200 0.5102 0.2017 0.3806 0.3365 0 0.1000 0.5000 0.2000 0.4800 0.2100 0.5500 0.1100 0.5700每行开头的整数是类别ID后面8个浮点数依次是四个顶点的归一化x、y。如果输出长这样格式层面就对了。这里我遇到过一个问题某些标注工具保存的angle是字符串形式比如“-26.57”但有些版本会存成“-0.4636”这种弧度值。脚本里float转换不会报错但角度差了一个数量级转出来的框全错。建议在批量转换前先写一个检查逻辑统计所有angle的取值范围如果绝对值都小于3说明可能是弧度需要乘以180/π转成度再算。4. 验证这一步千万别省可视化核对转换结果4.1 可视化脚本把txt画回原图格式转换这件事最容易出现“数据看起来没问题训练出来完全不对”的情况。所以强烈建议转换完先可视化一批样本把txt里的四角点画回到原图上和rolabelimg里原始标注对比。下面是一段快速可视化代码用OpenCV画旋转框import cv2 import numpy as np def draw_obb_from_txt(image_path, txt_path, classes, save_pathNone): img cv2.imread(image_path) h, w img.shape[:2] with open(txt_path, r, encodingutf-8) as f: lines f.readlines() colors [(0, 0, 255), (0, 255, 0), (255, 0, 0), (0, 255, 255), (255, 255, 0)] for line in lines: parts line.strip().split() if len(parts) 9: continue cls_id int(parts[0]) pts np.array([[float(parts[i]) * w, float(parts[i 1]) * h] for i in range(1, 9, 2)], dtypenp.int32).reshape(-1, 2) cv2.polylines(img, [pts], isClosedTrue, colorcolors[cls_id % len(colors)], thickness2) cx int(pts[:, 0].mean()) cy int(pts[:, 1].mean()) cv2.putText(img, classes[cls_id], (cx, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.8, colors[cls_id % len(colors)], 2) if save_path: cv2.imwrite(save_path, img) else: cv2.imshow(check, img) cv2.waitKey(0) cv2.destroyAllWindows()使用的时候传入原图路径、转换后的txt路径和classes列表跑个十几张典型样本一张张看框是否贴合目标。4.2 怎么看验证结果看可视化结果时要重点看三件事第一框的角度对不对。比如一艘斜靠岸边的船转换后的框应该和船身方向一致不能是水平的也不能是镜像的。如果角度反了框的四个顶点会在目标外侧翻转一眼就能发现。第二框的尺寸对不对。长条形目标转换后的框仍然应该是长条形w和h不能互换。如果输出框变成了转90度的形状说明w、h和angle的对应关系没弄对。第三框在边缘目标上的表现。目标贴近图像边界时旋转框的顶点可能落在图像外面这是正常现象YOLO训练能容忍略微越界的坐标。但如果你看到框的整体中心偏移得离谱那多半是XML里的cx、cy读错了或者图片宽高算错导致归一化除错了。4.3 批量抽检的建议不要只验证一张也不要每张都看。按我的习惯比例大概在5%到10%左右而且要有意识地挑选不同类别、不同旋转角度、不同位置的目标。比如一个遥感数据集有1000张抽50张覆盖所有类别每张图里框多且密集这样的验证就很有代表性。如果批量可视化不方便可以做一个更粗糙但有效的自动化检查统计所有txt里坐标值的范围。正常情况下绝大多数点在0到1之间如果大量出现负数或者超过1说明边缘目标很多或者转换有问题。还可以输出每个txt的行数和对应XML的object数量做对比数量不一致的文件重点排查。我在项目里就是这么干的先自动统计再看十几张图确认没问题后才放心把数据交给训练脚本。这一步省下的时间远大于它花掉的时间。5. 踩坑实录与常见问题速查5.1 角度是弧度还是角度这是出现频率最高的坑。rolabelimg保存的robndbox的angle大多数情况下是角度单位是度OpenCV定义范围[-90, 0)。但有些人拿到的数据经过二次处理或者标注工具版本不同angle可能是弧度。判断方法很简单把所有XML的angle打印出来看看数值分布。如果集中在-90到0之间那是角度如果集中在负的零点几到1之间那多半是弧度如果出现0到180的正值可能是DOTA约定。如果是弧度在调用rbox_to_4points之前先做一次转换angle_deg math.degrees(angle_rad)如果你已经把错误的数据转完并训练了一版模型也别急只要原始XML还在重新用修正后的脚本转一遍重新训练就行。原始标注数据永远不要丢这是吃一堑长一智的教训。5.2 四角点顺序不对或者图形交叉YOLO OBB对四点的顺序有要求吗严格说YOLOv8官方训练代码会重新处理和校验但如果你要拿去别的框架或者做数据可视化、转成DOTA格式做评测四点的顺序就必须规范。DOTA官方格式里四个点是按顺时针排列的从哪个顶点开始没有硬性规定但必须首尾相连不交叉。如果你发现可视化时框的边交叉了也就是画出来像一个蝴蝶结而不是矩形那说明四点的连接顺序有问题。可以用下面的简单算法把所有点按极角排序得到一个规范的顺时针顺序import numpy as np def sort_clockwise(points): pts np.array(points) cx, cy pts.mean(axis0) angles np.arctan2(pts[:, 1] - cy, pts[:, 0] - cx) idx np.argsort(angles) # 按角度升序图像坐标下得到顺时针/逆时针自己调一次方向 return pts[idx].tolist()注意图像坐标y向下arctan2排序出来的方向可能是逆时针需要结合可视化结果微调一下反转列表即可。这属于“排序约定”的问题不同工具之间经常不一致我的建议永远是以可视化结果为准不要迷信某段代码说自己是“顺时针”。5.3 水平框与旋转框混用如果你的标注人员在标注时偷懒一部分目标标了旋转框一部分目标只标了水平框那么XML里同一个object下robndbox和bndbox都存在但你无法确定哪些目标是真的旋转框、哪些是水平框“碰巧”也生成了外接矩形。我的处理方式是用面积和长宽双条件判断把robndbox计算的w和h与bndbox计算的宽高对比如果两者接近比如面积比例在0.9到1.1之间说明这个目标本来是水平框robndbox只是外接的一个等价表示如果比例明显小于1说明robndbox比bndbox小得多是真正的旋转框。实际数据里旋转框的robndbox面积通常显著小于bndbox这个特征比较可靠。不过在转换脚本里我仍然维持“优先robndbox”的策略因为即使目标是水平框robndbox表达也是正确的旋转框特例角度为0或接近0不影响训练。5.4 类别映射错位多类别数据集最容易翻车的地方。第一版脚本按字母序生成classes.txt第二版如果有人在前面加了一个类所有类别ID全部后移一位旧标签全部错位模型输出完全混乱。这种情况我建议在任何时候都不要手动改classes.txt的顺序新增类别永远追加到文件末尾。同时在转换脚本里加一个校验如果遇到类别不在classes.txt里不要静默忽略直接报错或者打印警告让你知道数据里出现了新类别。5.5 多源数据合并时的约定差异这次我处理的是一个混合来源的数据集一部分是rolabelimg标的一部分是从开源遥感数据转出来的保存格式不完全一样。比如有的XML里robndbox的angle写的是90度到180度之间的正值有的数据集里w和h的语义和rolabelimg反过来。这些数据合并前必须先统一到同一种坐标约定我最后是把所有数据都转成DOTA四点格式作为中间格式再统一转成YOLO OBB的txt这样两边约定不一致的问题就被中间格式消化掉了。5.6 常见问题速查表现象可能原因解决办法角度反向目标框歪到另一侧角度定义混淆OpenCV负角被当成DOTA正角修正角度定义保持原始负角框变瘦或者变胖看上去转了90度w和h与angle的对应关系错了确认angle对应wh是垂直边坐标全部接近0或全部接近1归一化时用错了图像宽高检查XML的width/height不要用txt顺序错位画出的框交叉成蝴蝶结四角点顺序错乱用极角排序重新排列四点训练时loss为NaN标签有Inf或者过大的异常坐标检查XML里是否出现异常框某些图片没有txt文件图片名和XML名对不上用XML的filename读取并做同名兜底全部类别ID都是0classes.txt顺序和预期不一致用旧的classes.txt将旧类别文件保留前三项是我在多次转换过程中真实遇到过的尤其是w和h对应关系因为不同版本的rolabelimg在界面上显示的“长边”和“短边”有时候会让标注者产生误解导致标出来的angle对应的边和我们脚本里解析的w不一致。遇到这种情况不要盲目改脚本先打开rolabelimg重新看一眼那个目标原始框的朝向再用数值计算验证一遍哪个假设是对的。结尾一点个人经验做了这么多轮xml转txt我的习惯是保留一个“转换配置”的备忘录把数据来源、标注工具版本、角度约定、w/h语义、classes顺序这五条信息记下来。因为这类数据转换往往不是一次性工作过两周可能又新增了一批标注数据要合并到时候如果没有备注你真的会忘记当时是怎么处理的。另外建议大家把原始XML永远保留一份转换后的txt只是中间产物什么时候想换格式、换模型、做评测都还要回到原始标注上去。之前有人直接把XML删了后来要出DOTA格式提交结果只能对照着txt反推XML折腾得够呛。最后再分享一个小技巧转换完别急着删脚本把脚本里的核心函数单独抽出来留着下次复用。我这次就是直接把之前的转换函数拿过来改了个classes路径五分钟搞定新数据批处理跑完顺手可视化抽检一遍整个过程稳稳当当。工具链这种东西越沉淀越好用。