旋转对象的有向边界框
标注四角 OBB,在 DOTA8 上训练 yolo26n-obb.pt,并将 xywhr 预测转换为稳定的方向轴,避免陷入角度约定的困扰。
港口中的船只、传送带上的包裹,以及俯视图中的停车场车辆都有一个共同问题:紧密排列的旋转对象周围若使用轴对齐边界框,框之间会彼此重叠并包含大量背景,导致 NMS 删除真实对象、计数偏低。有向边界框使用旋转矩形贴合每个对象,可解决密集场景中的计数问题,并直接提供角度。角度约定很容易让几乎所有初次集成的人出错——在本课的港口图像中,第一艘船的报告旋转角度为 −15°,但船身实际沿着 75° 的方向延伸。
实践操作
何时需要考虑旋转#

姿势估计与 OBB介绍了何时应选择 OBB 而非检测。实际判断方法是:如果相邻对象周围的轴对齐边界框彼此重叠的面积比对象本身还大,NMS 就会抑制真实检测,导致计数偏低。航空影像是典型场景,因此预训练的 yolo26n-obb.pt 使用 imgsz=1024 在 DOTA 上训练。
| 轴对齐框 | 定向框 | |
|---|---|---|
| 贴合一艘倾斜 45° 的船 | 约一半是背景 | 贴合紧密 |
| 密集的旋转排列 | 边界框相互重叠;NMS 会删除真实对象 | 边界框清晰分离 |
| 提供角度 | 否 | 可以,作为方向轴(模 180°) |
| 结果字段 | result.boxes | result.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 标注。

在 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.832OBB 指标位于 metrics.box 中,但其背后的 IoU 是通过旋转矩形计算的。
分数很高,是因为 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 切片推理指南介绍了这种方法;在带有检测器的高速公路画面中,整图推理会漏掉许多远处的车辆:

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

常见问题#
症状:“航向”在相邻帧之间翻转 180°,或者船只看起来在倒着航行。原因:旋转 180° 后的矩形仍是同一个矩形,因此 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° 后具有对称性;在船头添加一个姿态关键点即可获得真实航向。