在遥感、无人机航拍、自动驾驶感知、工业质检这些领域待久了的同学多半都会有同样的体验从高处往下看目标压根没有几个是正正经经横着摆的。建筑物是斜的停靠的船是斜的农田地块是斜的连巡检路上拍到的设备也是斜的。用传统的水平框去圈它们框里一大半是背景两个目标挨得近一点水平框之间的重叠面积甚至能超过框本身。这种场景下YOLO26的旋转目标识别OBB能力就变成了刚需。这篇文章我就按自己实际操作的流程来讲一遍完整链路。数据集标注怎么做、标注结果怎么转换成 YOLO26 需要的 OBB 格式、训练和测试的命令怎么敲、遇到问题怎么排查全部用经历过的方式给你说清楚。面向的对象是已经熟悉 YOLO 基础目标检测、但第一次接触旋转框任务的同学如果你完全没跑过目标检测其实也不是看不懂我会把每个环节的关键词和坑都顺便解释到位。1. 先搞清楚旋转目标识别到底解决了什么问题1.1 水平框在斜目标面前的窘境普通目标检测输出的是一族“轴对齐矩形框”也就是 horizontal bounding box。这种框只有四根线上、下、左、右。它不关心目标本身是什么姿态只要求能把目标大致框住。这在行人、汽车、日常物体的检测里很好用因为这些目标通常竖直摆着水平框已经够紧凑。但到了航拍场景问题就变了。一架飞机斜着停在跑道上一个水平框要把机头到机尾全部包住结果就是框的最左和最右两端伸出去一大截背景。更麻烦的是如果两架飞机离得近各自的水平框会大面积交叠后处理阶段做非极大值抑制NMS时很容易把其中一架误删掉。就算没误删两个水平框的 IoU 一算重合度很高模型在训练时也会困惑“这到底算一个目标还是两个目标”。这种情况下水平框的定位精度和召回率都会明显打折。旋转目标识别做的事情就是允许检测框跟着目标一起“转”。框不再是四条水平垂直线而是顺着目标主轴方向包住它。这样框内背景极少两个斜目标即便挨得很近旋转框之间的 IoU 也会比水平框小得多漏检和误删概率大幅下降。1.2 OBB 任务里的框到底怎么表示旋转框的标准英文名是 Oriented Bounding Box缩写就是 OBB。与水平框的四参数(x_center, y_center, width, height)不同OBB 有两种常见表示方式。第一种是带角度表示(cx, cy, width, height, angle)多了一个旋转角度。这个角度定义在哪个坐标系、逆时针还是顺时针、弧度还是角度不同框架之间并不完全一致这也是换框架时最容易出 bug 的地方。第二种是四角点表示(x1, y1, x2, y2, x3, y3, x4, y4)直接给出四边形四个顶点坐标。YOLO 系列 OBB 任务使用的就是这种表示。它的好处是直观、无角度歧义坏处是四个点的顺序必须全局统一否则同一个框会训练出完全不同的特征。在 YOLO 的 OBB 输出中每个目标是一个 68 维的向量。60 维是 COCO 的 80 类分类得分按默认配置8 维是四个角点各自相对网格的偏移量。训练时损失函数会同时监督分类和四个角点的坐标回归旋转框的回归损失通常用旋转感知的变体比如 ProbIoU 一类因为普通水平框的 IoU 计算对旋转框不起作用。1.3 为什么选 YOLO26 来做这件事YOLO26 是 YOLO 系列较新版本中带 OBB 能力的一代。选择它的理由其实很接地气不用自己写网络结构不用自己写训练循环连评估指标都是现成的。只要准备好数据和标注几条命令就能跑完整流程。对我来说它最大的价值在于OBB 任务在 YOLO 系列里不是独立分支而是和水平框任务共用一套工程框架。数据加载、数据增强、混合精度训练、多 GPU 训练、模型导出这些都是现成的少踩很多工程细节的坑。2. 数据集标注实操一步步把旋转框画出来2.1 标注工具怎么选一提标注很多人第一反应是 LabelImg 或者 Labelme。LabelImg 本身支持矩形框但旋转框需要额外插件或专门版本Labelme 画的是任意多边形你自己得管理导出格式。更省心的方案是直接用 X-AnyLabeling它是开源跨平台标注工具原生支持旋转矩形angle rectangle并且能一键导出 YOLO-OBB 格式。家里的项目基本都是用它来标注旋转框的不用写额外的标签转换代码就能和 YOLO 的目录结构对齐。如果团队成员习惯用网页工具也可以用 CVAT 或 Roboflow它们同样支持旋转框只是账号和项目配置会多一些步骤。选工具我只有一个硬标准能不能导出四角点坐标。只要导出的是四个角点后面的转换都能自己搞定。2.2 安装与操作步骤X-AnyLabeling 安装很简单pip install x-anylabeling国内用户如果网络不稳可以到开源仓库的 Release 页面下载对应系统的打包版本。打开软件后先创建标注标签列表类似ship vehicle airplane然后打开一张待标图片在左侧工具栏选择“旋转矩形”工具。接下来画框的每一步都有讲究在目标中心附近按下鼠标左键确定第一个角点。沿着目标主轴方向拖动先确定一条边。调整宽度让四条边刚好贴合目标边界。松开鼠标后再微调四个角点保证没有多余背景。关键在于第二个点的位置。画旋转框时第二个点的方向决定了整个框的旋转角度。同一架飞机你从机头方向拉出来的框和从机翼方向拉出来的框虽然四条边都能贴合但在格式上会被模型认为是两种不同的框。所以标注规范里必须写清楚所有框都从同一参考方向开始拉。X-AnyLabeling 默认导出 YOLO-OBB 格式每行是class_id x1 y1 x2 y2 x3 y3 x4 y4四个坐标是归一化到 0 到 1 的相对值也就是像素坐标除以图片宽高。如果标注过程中发现导出的是绝对像素坐标也不用慌后续格式转换脚本里可以统一处理。2.3 标注规范和质检清单标注阶段我吃过的最大亏是“标注规范没写在纸面上”。几个人同时标注有人顺时针画框有人逆时针画框有人沿主轴画框有人沿图像坐标画框。结果训练 20 个 epoch 后验证指标纹丝不动把验证集预测结果画出来才发现同一类别目标有的框转了好几圈。标注前建议强制先做两件事。第一写一条不超过十行的标注规范明确角点顺序、起始方向、是否允许截断框跨边界目标怎么处理、小目标不小于几个像素。第二先让全部标注人员各自标 10 张图合并后可视化检查统一风格再正式开标。检测标注质量最简单的方法是把标注结果画回原图上肉眼检查。我习惯写一个几十行的可视化脚本读取图片和标签把四个角点用cv2.polylines画出来随机抽 20% 的图片拼一张大图一个人五分钟就能扫完一批质量。另外那句老话还是要提类别分布一定要提前统计。无人机航拍数据里“建筑物”可能占了大头“船”只有几百个最后指标出来船类的 mAP 就是低得没法看。不是模型不行是样本不平衡。哪怕后面可以加采样策略标注阶段把各类别数量拉平均一点能省很多事。3. 数据格式转换把你手里的标注转成 YOLO-OBB3.1 先吃透目标格式在动手转换前先把 YOLO-OBB 格式的结构彻底弄懂后面踩坑基本都能从这里推导出答案。每个图片对应一个 txt 文件文件与图片同名后缀不一样图片是.jpg标签是.txt。每一行一个目标格式是class_id x1 y1 x2 y2 x3 y3 x4 y4class_id是从 0 开始的整数。四个点是旋转矩形的四个角按顺序连起来是一个凸四边形坐标全部归一化到 0 到 1 之间。注意一个问题YOLO-OBB 官方文档说明四点的顺序不需要严格是顺时针还是逆时针因为在加载数据时可以通过计算外积保证一致性。但为了保险起见你从标注端开始就统一成同一方向后续所有脚本都会好写很多。3.2 DOTA 格式转 YOLO-OBBDOTA 是遥感旋转目标检测最常用的数据集格式它和 YOLO-OBB 的文本格式长得非常像差别有两点坐标是像素绝对值不是归一化值类别是文本字符串不是整数 ID。DOTA 的一行长这样ship 100 200 300 200 300 400 100 400转换逻辑就很直接读图片宽高把所有坐标除以对应宽高把类别字符串映射成整数 ID写完一行。脚本如下import os from pathlib import Path CLASS_MAP {ship: 0, vehicle: 1, airplane: 2} def dota_to_yolo_obb(dota_txt_path, img_w, img_h, out_txt_path): lines [] with open(dota_txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 9: continue cls_name parts[8] if cls_name not in CLASS_MAP: continue cls_id CLASS_MAP[cls_name] coords list(map(float, parts[:8])) x coords[0::2] y coords[1::2] norm_coords [] for xi, yi in zip(x, y): norm_coords.append(xi / img_w) norm_coords.append(yi / img_h) lines.append(f{cls_id} .join(f{v:.6f} for v in norm_coords)) with open(out_txt_path, w, encodingutf-8) as f: f.write(\n.join(lines))注意 DOTA 原版的四个点顺序可能和 YOLO 期望的顺序不一致如果转换后训练时 loss 不稳定可以考虑把所有点按顺时针重排。重排方法在下一节的通用脚本里。3.3 LabelMe 多边形转 YOLO-OBBLabelMe 导出的 JSON 里每个目标的points是一个二维数组里面可能不止四个点因为你标注时随手画的可能是五边形六边形。旋转框任务需要的是矩形四角点所以第一步是把任意多边形转成最小外接旋转矩形用 OpenCV 的cv2.minAreaRect一条龙搞定import json import cv2 import numpy as np def labelme_to_yolo_obb(json_path, img_w, img_h, out_txt_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: label shape[label] if label not in CLASS_MAP: continue points np.array(shape[points], dtypenp.float32) # 获取最小外接旋转矩形 rect cv2.minAreaRect(points) # boxPoints 返回四个角点顺序不固定 box cv2.boxPoints(rect) # 重排为顺时针先算中心再算角度 cx, cy rect[0] angles np.arctan2(box[:, 1] - cy, box[:, 0] - cx) order np.argsort(angles) box_sorted box[order] # 归一化 box_norm box_sorted / np.array([img_w, img_h]) flat list(box_norm.reshape(-1)) lines.append(f{CLASS_MAP[label]} .join(f{v:.6f} for v in flat)) with open(out_txt_path, w, encodingutf-8) as f: f.write(\n.join(lines))cv2.minAreaRect本身算出来就是一个带角度的矩形boxPoints给出四个角点但顺序是不保证的。我用np.arctan2把所有点按绕中心的方位角排序这样得到一个顺序确定的外围四边形。你在自己项目里完全可以直接用但记得把CLASS_MAP换成自己的类别映射表。3.4 数据划分与目录整理训练前还有一项容易被忽略的活数据集划分。YOLO 的目录结构要求 train 和 val 分开建议在转换脚本里同时完成dataset/ images/ train/ val/ labels/ train/ val/ data.yaml划分时我建议先洗牌再切分避免同一个类别的目标全部出现在某个集合里。只放标签文件不够必须图片和 txt 一一对应。很多训练报了 valid 阶段跑出 NaN查下来就是某个 val 图片没有对应标签文件加载器读到了空数据。划分脚本也简单用random.shuffle洗牌后按比例分配。4. 训练配置与实操把 YOLO26 的 OBB 模型跑起来4.1 写好 data.yaml数据准备好后配置文件是关键。新建data.yamlpath: ./dataset train: images/train val: images/val names: 0: ship 1: vehicle 2: airplanepath写的是针对当前工作目录的相对路径也可以是绝对路径。names里的顺序必须和标注时的class_id完全对应一旦换了顺序模型训练出来的类别语义就乱了。这里的常见错误是漏掉names或漏掉类别名。YOLO 会直接报错或者全部当成背景类训练时 loss 一直不降。4.2 训练命令与关键参数YOLO 的 OBB 训练命令如下yolo obb train \ datadataset/data.yaml \ modelyolo26n-obb.pt \ epochs100 \ imgsz1024 \ batch16 \ lr00.01 \ patience30其中model参数可以填两种东西。一种是预训练权重训练时会下载权重文件然后继续微调另一种是yolo26n-obb.yaml这种模型结构配置文件此时会使用随机初始化权重从头训练。建议两种思路都尝试一遍预训练权重收敛快效果好随机初始化从头训练适合目标与自然图像差异极大的情况比如某些工业 X 光图。几个关键参数我根据自己的经验和网络上的普遍实践给你建议参数建议值理由imgsz1024 或 1280航拍小目标多输入分辨率太小会直接丢失细节batch16 左右具体看显存32G 显存可以 168G 建议 4epochs100 起步OBB 比水平框任务收敛略慢可以跑到 300patience30 或 50早停机制防止后期过拟合耗时optimizerauto默认自动切换不用刻意改device0使用第一张显卡4.3 遥感大图的切块处理无人机拍出来的单张影像动不动是 5000×5000 甚至上万像素。直接把这个分辨率喂给 YOLO 是不现实的显存撑不住即便硬跑目标占整张图的比例也太小。标准做法是把大图切成小块训练通常叫滑动窗口裁剪。我常用的切块策略有两条固定窗口大小一般取 1024×1024 或 1280×1280在保证目标清晰的前提下尽量小一点。重叠切块相邻窗口之间重叠一点比如 stride 设为 768 或 1024避免目标恰好被切在窗口边缘导致被截断。标签文件也要跟着裁剪。规则是这样窗口内保留完整落在窗口里的目标如果目标有超过一半面积在窗口内也保留但要把坐标裁剪到窗口边界。训练前可视化几张看看确认标签没有错位再开始训。4.4 训练过程监控与结果解读训练开始后终端会输出一个进度条里面有一串指标。对 OBB 任务最需要关注的损失字段是分类损失和旋转框回归损失。如果这两个值在稳步下降说明模型在学东西如果 loss 震荡剧烈可能是学习率太大或标注数据里噪声太多。训练完成后会在runs/obb/train下生成一堆文件。weights/best.pt是验证集上 mAP 最高的模型weights/last.pt是最后一个 epoch 的模型。日常推理和部署优先用best.pt。评估指标里面mAP50表示 IoU 阈值为 0.5 时的平均精度mAP50-95是多个 IoU 阈值的平均更严格但旋转框本身 IoU 计算比水平框更敏感所以mAP50-95偏低是正常现象不用紧张。5. 测试与推理验证模型效果并应用到新图上5.1 对单张图做推理推理代码很简单from ultralytics import YOLO model YOLO(runs/obb/train/exp/weights/best.pt) results model.predict( test_image.jpg, imgsz1024, conf0.25, ) for result in results: if result.obb is None: continue boxes result.obb.xyxyxyxy.numpy() # 四角点坐标像素单位 classes result.obb.cls.numpy() # 类别 ID confs result.obb.conf.numpy() # 置信度result.obb是 OBB 任务特有的输出对象里面包含角点、置信度、类别。xyxyxyxy就是一个N×8的数组。如果你的model本身是从 OBB 权重加载的它天然就是 OBB 输出如果你手上只有水平框 YOLO 权重推理时就要注意用model YOLO(权重路径, taskobb)来明确任务类型否则可能输出不对。5.2 在超大图上做探测滑窗推理与合并既然训练时用了滑块裁剪推理时也必须同样方式处理否则大图上的目标会因为分辨率不够而漏检。推理阶段的流程是把大图按窗口切成小块每个小块过一遍模型得到带角度框然后把所有框的坐标加回偏移量映射回原始大图坐标系最后做一个全局合并。合并时的关键步骤是 NMS。由于是旋转框普通的水平 NMS 不管用需要用旋转框专有的 NMS 逻辑在 ultralytics 内部有实现。简单场景下可以按置信度排序后循环判断 IoU 重叠度超过阈值就删掉低置信度的框。为了减少跨块重复预测重合度阈值可以稍微调高一点比如 IoU 超过 0.6 就认为是一个目标。5.3 推理结果可视化与后续处理可视化用 OpenCV 直接把四个角点画出来即可import cv2 import numpy as np image cv2.imread(test_image.jpg) for result in results: if result.obb is None: continue for pts in result.obb.xyxyxyxy.numpy(): pts pts.astype(np.int32).reshape(-1, 1, 2) cv2.polylines(image, [pts], True, (0, 255, 0), 2) cv2.imwrite(vis_result.jpg, image)如果你要把结果交付给下游 GIS 系统可以考虑把四个角点导出成 GeoJSON 中的 Polygon。需要注意的是角点坐标此时是像素坐标如果影像带了地理参考还需要用原始影像的仿射变换参数把像素坐标转换成地理坐标这一步常被忽略。6. 常见问题与排查技巧实录6.1 训练了很久mAP 一直为 0我见过最多的不是模型问题而是标签和图像对不上。具体表现包括标签坐标没有归一化数值大到离谱类别 ID 从 1 开始而不是从 0 开始图片和 txt 名字不完全匹配。排查次序建议是先看一张验证图片用result.show()把预测结果叠加显示出来再手动写脚本把 gt 标签画上去。两者错位就能立刻看到。6.2 显存爆掉OBB 模型通常配合高分辨率输入显存消耗大是常态。优先把batch从 16 降到 8 或 4然后看imgsz能不能从 1280 降到 1024如果还不行考虑把图像缩放后再做切块。还有一个偏方是开启梯度累积在 ultralytics 里通过参数nbs64可以模拟更大的批次大小显存占用不变训练稳定性还能提升。6.3 训练 loss 下降但不收敛验证集指标抖动这种情况大多是学习率太高或者标注数据里混了大量方向不一致的样本。检查数据增强阶段的可视化输出看旋转增强后的框和原始框是否还能贴合目标。OBB 任务中有些数据增强会旋转图像但不会旋转标签这样的增强会把标注方向搞乱导致损失增加。建议先关闭大部分增强跑 20 个 epoch 看看如果稳定收敛再慢慢打开增强。6.4 推理检测结果全图一片混乱如果推理阶段拿到的是空白结果或满屏小框优先怀疑“任务不对”。从一个不是 OBB 训练得到的权重直接加载模型不会输出 OBB 结构。需要用taskobb强制指定并且确保模型文件是 OBB 头结构的。6.5 坐标越界预测出来的角点出现负数或大于 1 的数值通常是你手动后处理时忘了归一化。YOLO 在推理时输出的是像素坐标但标签文件里的坐标是归一化的。如果要做可视化直接用像素坐标没问题如果要用标签对比就必须先除以宽高。我自己的实践经验是OBB 项目里 80% 的坑都出在“格式不一致”和“标注方向不统一”上。只要把这两关守住后续训练和推理基本就是流水线操作了。另外初次接触旋转目标识别的同学建议先用公开的 DOTA 抽取一个小子集把整套流程从标注到训练完整跑通一次再上自己的数据。这个成本很低但能让你在真正遇到问题时快速定位是数据问题、格式问题还是模型参数问题。后续如果要继续扩展可以在这个基础上尝试用 OBB 输出做旋转目标检测和语义分割的联合任务或者把模型导出为 ONNX 部署到嵌入式平台那也是另一个有意思的方向了。