صناديق الإحاطة الموجّهة للكائنات المدارة
وسّم صناديق OBB ذات الزوايا الأربع، ودرّب yolo26n-obb.pt على DOTA8، وحوّل تنبؤات xywhr إلى محور اتجاه ثابت دون الوقوع في التباس اصطلاحات الزوايا.
تشترك السفن في الميناء والطرود على السير والسيارات في موقف تُرى من الأعلى في مشكلة واحدة: تتداخل صناديق المحاذاة المحورية حول الكائنات المدارة والمتراصة بإحكام، وتبتلع الخلفية؛ لذا يحذف NMS كائنات حقيقية وتأتي الأعداد أقل من الواقع. تلائم صناديق الإحاطة الموجّهة كل كائن بمستطيل مدوّر، ما يحسن العد في المشاهد الكثيفة ويمنحك زاوية مباشرة. لكن الزاوية ترتبط باصطلاحات تُربك تقريبًا كل عملية دمج أولى — ففي صورة الميناء في هذا الدرس، تسجل السفينة الأولى دورانًا قدره −15°، مع أن هيكلها يمتد بزاوية 75°.
عملي
متى يكون الدوران مهمًا#

يوضح تقدير الوضعيات وOBB متى ينبغي اختيار OBB بدلًا من الكشف. والاختبار العملي هو: إذا تداخلت صناديق الإحاطة المحورية حول الكائنات المتجاورة أكثر من تداخل الكائنات نفسها، فسيحذف NMS اكتشافات حقيقية وستنخفض الأعداد عن الواقع. وتُعد الصور الجوية الحالة الكلاسيكية، ولهذا دُرّب النموذج yolo26n-obb.pt المدرب مسبقًا على DOTA باستخدام imgsz=1024.
| صندوق محاذٍ للمحاور | مربع موجّه | |
|---|---|---|
| ملاءمة سفينة بزاوية 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.832توجد مقاييس OBB في 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 موانئ. يكونresult.boxesهوNoneفي نماذج OBB، لذا يجب أن يبدّل كود العد المكتوب للكشف الحقول. 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°؛ وتتيح نقطة مفتاحية في وضعية الجسم على المقدمة تحديد الاتجاه الفعلي.