Ultralytics YOLO27: Tham gia danh sách chờ.
Get Started
Bài họctrung cấp

Bounding Box Xoay cho Đối tượng Xoay

Gán nhãn OBB bốn góc, huấn luyện yolo26n-obb.pt trên DOTA8 và chuyển dự đoán xywhr thành trục hướng ổn định mà không mắc phải các quy ước về góc.

Tàu trong cảng, bưu kiện trên băng chuyền và ô tô trong bãi đỗ nhìn từ trên xuống có chung một vấn đề: bounding box căn theo trục bao quanh các đối tượng xoay và nằm sát nhau sẽ chồng lấn lên nhau, đồng thời chiếm nhiều nền, khiến NMS xóa nhầm đối tượng thật và số lượng đếm được thấp hơn thực tế. Bounding box xoay ôm sát từng đối tượng bằng hình chữ nhật xoay, khắc phục vấn đề đếm trong cảnh dày đặc và cung cấp sẵn góc xoay. Góc này đi kèm các quy ước khiến hầu hết lần tích hợp đầu tiên gặp trục trặc — trong ảnh cảng ở bài học này, con tàu đầu tiên có góc xoay −15°, nhưng thân tàu nằm theo hướng 75°.

Kết quả

Huấn luyện yolo26n-obb.pt trên dota8.yaml, đếm số detection theo class từ result.obb và chuyển xywhr của từng dự đoán thành trục cạnh dài trong khoảng [0, 180) độ.

Lộ trình cấp tốc
Nếu bạn đã nắm rõ cách thực hiện, đây là phiên bản tóm tắt.
  1. Một hàng nhãn: class, sau đó là bốn góc x1 y1 … x4 y4, được chuẩn hóa và sắp theo thứ tự quanh chu vi.

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

  3. Đọc result.obb.xywhr (góc xoay tính bằng radian) hoặc .xyxyxyxy (các góc); kết quả OBB không điền result.boxes.

  4. Chuẩn hóa hướng theo cạnh dài: cộng 90° khi w < h, sau đó lấy góc modulo 180°.

Thực hành

Khi hướng xoay quan trọng#

Ultralytics YOLO đặt bounding box xoay khít quanh ba du thuyền neo đậu trong ảnh chụp từ trên cao, mỗi box được gắn nhãn Ship cùng độ tin cậy

Ước tính pose và OBB giải thích khi nào nên chọn OBB thay cho detection. Cách kiểm tra thực tế: nếu bounding box căn theo trục quanh các đối tượng lân cận chồng lấn lên nhau nhiều hơn chính các đối tượng đó, NMS sẽ loại bỏ detection đúng và số lượng đếm được sẽ thấp hơn thực tế. Ảnh chụp từ trên cao là trường hợp điển hình, đó là lý do model pretrained yolo26n-obb.pt được huấn luyện trên DOTA tại imgsz=1024.

Bounding box căn theo trụcBounding box định hướng
Khít quanh tàu ở góc 45°Khoảng một nửa là nềnKhít sát
Các hàng đối tượng xoay, dày đặcCác box chồng lấn; NMS loại bỏ đối tượng thậtCác box tách biệt rõ ràng
Cung cấp góckhôngCó, dưới dạng một trục (modulo 180°)
Trường kết quảresult.boxesresult.obb

Định dạng nhãn xyxyxyxy#

Một hàng nhãn OBB gồm chỉ số class, theo sau là bốn góc đã chuẩn hóa và sắp theo thứ tự quanh chu vi. Một hàng thực tế trong DOTA8 (class 1 là ship):

1 0.813477 0.523437 0.814453 0.504882 0.87207 0.504882 0.87207 0.524414

Đọc hàng này thành bốn điểm: (0.813, 0.523) → (0.814, 0.505) → (0.872, 0.505) → (0.872, 0.524). Con tàu này gần như nằm ngang, rộng khoảng 0.059 và cao 0.019 so với kích thước ảnh. Bên trong, Ultralytics chuyển mỗi bộ góc thành tâm, chiều rộng, chiều cao và góc xoay (xywhr) bằng cách khớp hình chữ nhật có diện tích nhỏ nhất, vì vậy các tứ giác hơi lệch khỏi hình chữ nhật do công cụ gán nhãn tạo ra vẫn dùng được để huấn luyện. Hướng dẫn về dataset OBB mô tả định dạng này, còn convert_dota_to_yolo_obb trong ultralytics.data.converter chuyển đổi nhãn DOTA gốc.

Trình biên tập gán nhãn Ultralytics Platform trên ảnh chụp DOTA8 từ trên cao, với các box xoay quanh sân tennis, xe nhỏ và hồ bơi

Huấn luyện trên DOTA8#

DOTA8 là một bộ dữ liệu hộp định hướng nhỏ được xây dựng để kiểm thử pipeline:

Ảnh ghép mosaic huấn luyện DOTA8 gồm các ô ảnh từ trên không: các hàng thuyền neo đậu, sân tennis, bãi đỗ xe và máy bay trên sân đỗ, mỗi đối tượng có hộp định hướng và chỉ số lớp

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

Các chỉ số OBB nằm trong metrics.box, nhưng IoU phía sau chúng được tính giữa các hình chữ nhật đã xoay.

DOTA8 là bài kiểm tra nhanh

Điểm số cao vì DOTA8 là một phần của dữ liệu pretraining. Điểm số này chứng minh pipeline chạy được, chứ không nói lên điều gì về hình ảnh của bạn.

Đọc các dự đoán và chuẩn hóa góc#

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

Giải thích từng dòng:

  • xywhr: (173, 5) — 173 detection, mỗi detection có tâm x, tâm y, chiều rộng, chiều cao tính bằng pixel và góc xoay tính bằng radian. xyxyxyxy chứa các hộp tương tự dưới dạng bốn điểm góc, sẵn sàng cho cv2.polylines.
  • Counter đếm từ result.obb.cls: 168 tàu và 5 bến cảng. result.boxes là None trên các model OBB, vì vậy mã đếm được viết cho detection phải chuyển đổi trường.
  • raw so với axis. Chiếc tàu đầu tiên có kích thước 33 × 108 ở góc −15°: w của tàu này là cạnh ngắn, vì vậy thân tàu nằm dọc theo −15° + 90° = 75°. Chiếc thứ hai có kích thước 116 × 33 ở góc 21°, với cạnh dài được ghi trước. Đọc góc thô sẽ khiến trục của chiếc tàu đầu tiên sai 90°; sau khi chuẩn hóa, góc của mọi tàu đều biểu thị cùng một hướng và có thể so sánh giữa các tàu cũng như giữa các frame.

Quy tắc này xuất phát từ source đã cài đặt: góc xoay được dự đoán nằm trong [−π/4, 3π/4) — xấp xỉ −45° đến 135° — và cùng một hình chữ nhật có thể là (w, h, θ) hoặc (h, w, θ + 90°). Đừng bao giờ mặc định w là cạnh dài.

Đối tượng nhỏ cần tile, không phải thu nhỏ ảnh#

Đối tượng chụp từ trên không thường nhỏ, và việc validation hoặc triển khai ở độ phân giải thấp hơn imgsz dùng khi huấn luyện sẽ nhanh chóng làm giảm recall. Chia ảnh vệ tinh thành các tile chồng lấp nếu ảnh lớn hơn kích thước đầu vào của model, thay vì thu nhỏ ảnh; nếu không, một chiếc ô tô 30 pixel sẽ biến thành vệt mờ 5 pixel. Hướng dẫn inference theo tile SAHI trình bày kỹ thuật này; trên một frame đường cao tốc dùng detector, inference trên toàn ảnh bỏ sót nhiều phương tiện ở xa:

Frame đường cao tốc với các detection từ inference toàn ảnh, bỏ sót nhiều phương tiện nhỏ ở xa

còn inference theo tile phát hiện được chúng:

Cùng frame đường cao tốc với inference theo tile SAHI, phát hiện được nhiều phương tiện nhỏ ở xa hơn

Các lỗi thường gặp#

Không phân biệt được mặt trước hay mặt sau

Triệu chứng: "hướng" lật 180° giữa các frame, hoặc tàu dường như chạy lùi. Nguyên nhân: hình chữ nhật xoay 180° vẫn là cùng một hình chữ nhật, nên OBB chỉ cho biết trục chứ không cho biết hướng. Cách khắc phục: nếu cần biết tàu hoặc xe nâng đang hướng về phía nào, hãy thêm keypoint pose ở mũi tàu hoặc đầu càng, hoặc dùng classifier trên crop.

So sánh đường cong validation thô với các lần chạy detection

Triệu chứng: Các đường cong precision-recall của OBB trông khác với các lần chạy detection trên dữ liệu tương tự. Nguyên nhân: Validation OBB mặc định dùng conf=0.01, trong khi các tác vụ khác dùng 0.001. Cách khắc phục: so sánh các giá trị mAP hoặc đặt rõ conf khi cần các đường cong độ chính xác-độ bao phủ tương ứng.

Dùng thử

Chạy obb.py, sau đó viết một script tính trục cạnh dài của từng tàu trong ảnh bến cảng, tìm trục phổ biến nhất với độ chính xác 10° gần nhất và đếm số tàu lệch hơn 30° so với trục đó. Hãy nhớ rằng 175° và 5° chỉ cách nhau 10°.

Điều kiện hoàn thành
Bạn đã hoàn thành bài học khi tất cả các điều kiện sau đều đúng.
  • Script của bạn in ra số tàu, trục phổ biến nhất và số tàu lệch hơn 30° so với trục đó.

  • Phép so sánh trục của bạn tính theo chu kỳ 180°, vì vậy 175° và 5° được tính là cách nhau 10°.

  • Bạn có thể giải thích vì sao OBB không thể phân biệt mũi tàu với đuôi tàu, và sẽ bổ sung gì nếu cần xác định hướng di chuyển.

Hiển thị lời giải
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

Phần lớn thuyền nằm song song ở góc khoảng 20°; 32 tàu lệch trục là các trường hợp ngoại lệ mà một quy tắc giám sát bến cảng sẽ đánh dấu để xem xét. OBB không thể phân biệt mũi tàu với đuôi tàu vì hình chữ nhật đối xứng khi xoay 180°; keypoint tư thế ở mũi tàu sẽ cung cấp hướng thực.

Tiếp theo là gì

Tiếp theo, chúng ta bỏ hoàn toàn bước định vị và huấn luyện một classifier cho toàn bộ ảnh và các crop.

Đăng nhập để lưu tiến độ