Ultralytics YOLO27: 加入候补名单。
Get Started
课程中级

旋转对象的有向边界框

标注四角 OBB,在 DOTA8 上训练 yolo26n-obb.pt,并将 xywhr 预测转换为稳定的方向轴,避免陷入角度约定的困扰。

港口中的船只、传送带上的包裹,以及俯视图中的停车场车辆都有一个共同问题:紧密排列的旋转对象周围若使用轴对齐边界框,框之间会彼此重叠并包含大量背景,导致 NMS 删除真实对象、计数偏低。有向边界框使用旋转矩形贴合每个对象,可解决密集场景中的计数问题,并直接提供角度。角度约定很容易让几乎所有初次集成的人出错——在本课的港口图像中,第一艘船的报告旋转角度为 −15°,但船身实际沿着 75° 的方向延伸。

学习成果

在 dota8.yaml 上训练 yolo26n-obb.pt,根据 result.obb 统计每个类别的检测数量,并将每个预测的 xywhr 转换为 [0, 180) 度范围内的长边方向轴。

快速通道
如果你已经熟悉相关内容,以下是简要版。
  1. 标签行:类别,然后是按周边顺序排列并归一化的四个角点 x1 y1 … x4 y4。

  2. yolo obb train data=dota8.yaml model=yolo26n-obb.pt epochs=1 imgsz=320.

  3. 读取 result.obb.xywhr(以弧度表示的旋转角度)或 .xyxyxyxy(角点);OBB 结果不会填充 result.boxes。

  4. 将方向归一化到长边:当 w < h 时加上 90°,然后对角度取模 180°。

实践操作

何时需要考虑旋转#

Ultralytics YOLO 有向边界框紧密贴合航拍照片中三艘停泊的游艇,每个框都标注为 Ship 并带有置信度

姿势估计与 OBB介绍了何时应选择 OBB 而非检测。实际判断方法是:如果相邻对象周围的轴对齐边界框彼此重叠的面积比对象本身还大,NMS 就会抑制真实检测,导致计数偏低。航空影像是典型场景,因此预训练的 yolo26n-obb.pt 使用 imgsz=1024 在 DOTA 上训练。

轴对齐框定向框
贴合一艘倾斜 45° 的船约一半是背景贴合紧密
密集的旋转排列边界框相互重叠;NMS 会删除真实对象边界框清晰分离
提供角度否可以,作为方向轴(模 180°)
结果字段result.boxesresult.obb

xyxyxyxy 标签格式#

一行 OBB 标签由类别索引和按周边顺序排列、经过归一化的四个角点组成。以下是一行真实的 DOTA8 标签(类别 1 是 ship):

1 0.813477 0.523437 0.814453 0.504882 0.87207 0.504882 0.87207 0.524414

将它读作四个点:(0.813, 0.523) → (0.814, 0.505) → (0.872, 0.505) → (0.872, 0.524)。这艘船几乎水平,宽度约为图像的 0.059,高度约为 0.019。在内部,Ultralytics 会通过拟合最小面积矩形,将每组角点转换为中心点、宽度、高度和旋转角度(xywhr),因此,即使标注工具输出的四边形略有不规则,模型仍可进行训练。OBB 数据集指南介绍了这一格式,而 convert_dota_to_yolo_obb 位于 ultralytics.data.converter 中,可转换原始 DOTA 标注。

Ultralytics Platform 标注编辑器显示一张 DOTA8 航拍图像,其中网球场、小型车辆和游泳池周围标注了旋转框

在 DOTA8 上训练#

DOTA8 是一个用于测试流水线的小型旋转框数据集:

DOTA8 训练拼图,展示了多行航空图像切片,包括停泊的船只、网球场、停车场和停机坪上的飞机,每个目标都带有旋转框和类别索引

from ultralytics import YOLO

model = YOLO("yolo26n-obb.pt")
model.train(data="dota8.yaml", epochs=1, imgsz=320, device="cpu")
metrics = model.val()
print(f"OBB mAP50: {metrics.box.map50:.3f}  mAP50-95: {metrics.box.map:.3f}")
OBB mAP50: 0.995  mAP50-95: 0.832

OBB 指标位于 metrics.box 中,但其背后的 IoU 是通过旋转矩形计算的。

DOTA8 是冒烟测试

分数很高,是因为 DOTA8 是预训练数据的一部分。它只能证明流程可以运行,不能说明你的图像表现如何。

读取预测结果并规范化角度#

import math
from collections import Counter

from ultralytics import YOLO

def long_side_axis(w, h, r):
    """Angle of the box's long side in degrees, in [0, 180), whichever side the model called width."""
    deg = math.degrees(r) + (90 if w < h else 0)
    return deg % 180

model = YOLO("yolo26n-obb.pt")  # trained on DOTAv1, 15 aerial classes
result = model("https://ultralytics.com/images/boats.jpg", verbose=False)[0]
obb = result.obb.cpu()

print("xywhr:", tuple(obb.xywhr.shape), " xyxyxyxy:", tuple(obb.xyxyxyxy.shape))
print(Counter(result.names[int(c)] for c in obb.cls.tolist()))
for (cx, cy, w, h, r), conf in list(zip(obb.xywhr.tolist(), obb.conf.tolist()))[:5]:
    print(f"{conf:.2f} center=({cx:.0f},{cy:.0f}) size={w:.0f}x{h:.0f} raw={math.degrees(r):4.0f} deg  axis={long_side_axis(w, h, r):4.0f} deg")
xywhr: (173, 5)  xyxyxyxy: (173, 4, 2)
Counter({'ship': 168, 'harbor': 5})
0.87 center=(619,850) size=33x108 raw= -15 deg  axis=  75 deg
0.86 center=(1650,937) size=116x33 raw=  21 deg  axis=  21 deg
0.86 center=(1105,571) size=124x35 raw=  21 deg  axis=  21 deg
0.86 center=(1596,1037) size=129x35 raw=  25 deg  axis=  25 deg
0.86 center=(825,940) size=34x99 raw= -15 deg  axis=  75 deg

逐行说明:

  • xywhr: (173, 5) — 173 个检测结果,每个结果都包含以像素为单位的中心 x、中心 y、宽度、高度,以及以弧度为单位的旋转角度。xyxyxyxy 保存了相同的框,形式为四个角点,可直接用于 cv2.polylines。
  • 计数器从 result.obb.cls 读取结果:168 艘船和 5 个港口。在 OBB 模型中,result.boxes 就是 None,因此为检测任务编写的计数代码必须切换字段。
  • raw 与 axis。第一艘船的尺寸为 33 × 108,角度为 −15°:它的 w 是短边,因此船身沿着 −15° + 90° = 75° 的方向延伸。第二艘船的尺寸为 116 × 33,角度为 21°,长边已经在前。直接读取原始角度会让第一艘船的轴向偏差 90°;规范化之后,每艘船的角度含义一致,可以在不同船只和帧之间进行比较。

这个规则来自已安装的源码:预测旋转角度位于 [−π/4, 3π/4) — 大约是 −45° 到 135° — 同一个矩形可以表示为 (w, h, θ) 或 (h, w, θ + 90°)。不要假设 w 是长边。

小目标需要切片,而不是缩小#

航空图像中的目标很小,验证或部署时使用低于训练 imgsz 的分辨率会迅速降低召回率。对于大于模型输入尺寸的卫星图像切片,应将其拆分为有重叠的切片,而不是缩小;否则,30 像素的汽车会变成 5 像素的模糊斑点。SAHI 切片推理指南介绍了这种方法;在带有检测器的高速公路画面中,整图推理会漏掉许多远处的车辆:

使用整图推理处理的高速公路画面,其中漏掉了许多小型远处车辆

而切片推理能找到这些车辆:

同一幅高速公路画面,使用 SAHI 切片推理后检测到了更多小型远处车辆

常见问题#

没有正面或背面之分

症状:“航向”在相邻帧之间翻转 180°,或者船只看起来在倒着航行。原因:旋转 180° 后的矩形仍是同一个矩形,因此 OBB 给出的是轴向,而不是航向。解决方法:如果你需要知道船或叉车的朝向,就在船头或叉尖添加姿态关键点,或者对裁剪图进行分类。

比较原始验证曲线与检测任务运行结果

症状: OBB 精确率-召回率曲线与相似数据上的检测任务运行结果不同。原因: OBB 验证默认使用 conf=0.01,而其他任务使用 0.001。解决方法:比较 mAP 值;如果需要匹配的精确率-召回率曲线,则显式设置 conf。

试试看

运行 obb.py,然后编写一个脚本,计算港口图像中每艘船的长边轴向,找出最常见的轴向(精确到最近的 10°),并统计与其相差超过 30° 的船只数量。记住,175° 和 5° 只相差 10°。

完成条件
满足以下所有条件,即表示你已完成本课程。
  • 你的脚本会输出船只数量、最常见的轴向,以及与该轴向相差超过 30° 的船只数量。

  • 你的轴向比较会按 180° 循环,因此 175° 和 5° 会被视为相差 10°。

  • 你可以解释为什么 OBB 无法区分船头和船尾,以及如果需要航向信息,你会添加什么。

显示解答
import math
from collections import Counter

from ultralytics import YOLO

result = YOLO("yolo26n-obb.pt")("https://ultralytics.com/images/boats.jpg", verbose=False)[0]
axes = []
for (cx, cy, w, h, r), cls in zip(result.obb.xywhr.cpu().tolist(), result.obb.cls.tolist()):
    if result.names[int(cls)] == "ship":
        axes.append((math.degrees(r) + (90 if w < h else 0)) % 180)  # long-side axis in [0, 180)

def axis_gap(a, b):
    """Smallest difference between two axes; 175 and 5 degrees are only 10 apart."""
    d = abs(a - b) % 180
    return min(d, 180 - d)

common = Counter(round(a / 10) * 10 % 180 for a in axes).most_common(1)[0][0]
off = [a for a in axes if axis_gap(a, common) > 30]
print(f"{len(axes)} ships, most common axis ~{common} deg, {len(off)} more than 30 deg off it")
168 ships, most common axis ~20 deg, 32 more than 30 deg off it

大多数船只大约平行于 20° 方向;偏离轴向的 32 艘船是港口监控规则会标记出来以供审查的异常目标。OBB 无法区分船头和船尾,因为矩形旋转 180° 后具有对称性;在船头添加一个姿态关键点即可获得真实航向。

接下来做什么

接下来,我们完全舍弃定位任务,训练一个用于整幅图像和裁剪图像的分类器。

登录以保存学习进度