Ultralytics YOLO27: انضم إلى قائمة الانتظار.
Get Started
الدرسمتوسط

صناديق الإحاطة الموجّهة للكائنات المدارة

وسّم صناديق OBB ذات الزوايا الأربع، ودرّب yolo26n-obb.pt على DOTA8، وحوّل تنبؤات xywhr إلى محور اتجاه ثابت دون الوقوع في التباس اصطلاحات الزوايا.

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

النتيجة

درّب yolo26n-obb.pt على dota8.yaml، واحسب عدد الاكتشافات لكل فئة من 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. طبّع الاتجاه إلى الضلع الطويل: أضف 90° عندما تكون w < h، ثم احسب باقي قسمة الزاوية على 180°.

عملي

متى يكون الدوران مهمًا#

صناديق إحاطة موجّهة من Ultralytics YOLO ملائمة بإحكام حول ثلاثة يخوت راسية في صورة جوية، مع تسمية Ship ومستوى الثقة لكل منها

يوضح تقدير الوضعيات وOBB متى ينبغي اختيار OBB بدلًا من الكشف. والاختبار العملي هو: إذا تداخلت صناديق الإحاطة المحورية حول الكائنات المتجاورة أكثر من تداخل الكائنات نفسها، فسيحذف NMS اكتشافات حقيقية وستنخفض الأعداد عن الواقع. وتُعد الصور الجوية الحالة الكلاسيكية، ولهذا دُرّب النموذج yolo26n-obb.pt المدرب مسبقًا على DOTA باستخدام imgsz=1024.

صندوق محاذٍ للمحاورمربع موجّه
ملاءمة سفينة بزاوية 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 موانئ. يكون 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 هذه التقنية؛ ففي إطار طريق سريع مع كاشف، يفوّت الاستدلال على الصورة كاملةً كثيرًا من المركبات البعيدة:

إطار لطريق سريع مع اكتشافات الاستدلال على الصورة كاملةً، الذي يفوّت كثيرًا من المركبات الصغيرة والبعيدة

ويعثر الاستدلال المقسم إلى مربعات عليها:

الإطار نفسه للطريق السريع باستخدام الاستدلال المقسم إلى مربعات من 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°؛ وتتيح نقطة مفتاحية في وضعية الجسم على المقدمة تحديد الاتجاه الفعلي.

ما الخطوة التالية

بعد ذلك، نتخلى تمامًا عن تحديد المواقع وندرّب مصنّفًا للصور الكاملة والقصاصات.

سجّل الدخول لحفظ التقدم