انتقل إلى المحتوى الرئيسي

الوحدة 8 — تتبّع الأجسام المتعدّدة في الفيديو

الوحدات السابقة عالجت الصور المنعزلة. لكن كاميرا التقاطع لا تُنتج صورًا، بل فيديو: 25 إلى 30 إطارًا في الثانية. لعدّ السيّارات التي تعبر التقاطع من الشرق إلى الغرب، لا يكفي الكشف: نحتاج إلى معرفة أنّ السيّارة في الإطار 43 هي نفسها التي كانت في الإطار 42. هذه هي مسألة التتبّع متعدّد الأجسام (Multi-Object Tracking، MOT).

المشكلة: ربط الكشوف بالمسارات

في كلّ إطار، الكاشف يُنتج قائمة من الصناديق. عبر الإطارات، يجب أن نُنشئ مسارات (tracks): تسلسلات من الصناديق تنتمي إلى الجسم نفسه، بمعرّف فريد.

المسار المثالي: السيّارة الحمراء تدخل الإطار في اليمين، تُعطى المعرّف #7، تحتفظ به طيلة عبورها التقاطع، ثم تخرج من اليسار. عندما تختفي، معرّفها لا يُعاد استخدامه لسيّارة أخرى.

المشاكل التي تجعل ذلك صعبًا:

  • الحجب (occlusion): سيّارة تمرّ خلف حافلة لخمسة إطارات، ثم تظهر مرّة أخرى. هل هي نفس المسار #7 أم مسار جديد؟
  • الكشوف المفقودة: الكاشف يُفوّت السيّارة في بعض الإطارات
  • الكشوف الخاطئة: انعكاس على زجاج يُنتج «سيّارة شبح»
  • حركة سريعة: سيّارة تقطع مسافة كبيرة بين إطارين، يصير التطابق المكاني غير مباشر

مرشّح كالمان: تنبّؤ الموضع التالي

مرشّح كالمان هو الأداة الكلاسيكية لتقدير حالة نظام ديناميكي عبر الزمن. في التتبّع، الحالة هي موضع الجسم وحجمه وسرعته:

x=(u,v,s,r,u˙,v˙,s˙)\mathbf{x} = (u, v, s, r, \dot{u}, \dot{v}, \dot{s})

مع u,vu, v المركز، ss المساحة، rr نسبة العرض/الارتفاع، والنقاط تعني المشتقّات الزمنية.

المرشّح يعمل في دورتين:

  1. التنبّؤ: بناءً على الحالة السابقة والسرعة، نتنبّأ بالموضع في الإطار التالي. سيّارة كانت في (200,300)(200, 300) بسرعة (10,0)(10, 0) ستكون قريبًا من (210,300)(210, 300).
  2. التحديث: نُقارن التنبّؤ بالكشف الفعلي في الإطار الجديد، ونُصحّح الحالة بمزج الاثنين حسب عدم اليقين في كلّ منهما.

فائدة كالمان هنا مزدوجة: يعطينا صناديق مُنعّمة (بغيره تتأرجح صناديق الكشف بسبب ضجيج النموذج)، وعندما يفشل الكشف في إطار، يستمرّ التنبّؤ بلا انقطاع.

SORT: البسيط الفعّال

SORT (Simple Online and Realtime Tracking، Bewley et al.، 2016) هو أوّل خوارزمية تتبّع أثبتت أنّ البساطة تُنافس التعقيد. الفكرة:

  1. لكلّ إطار جديد، شغّل الكاشف واحصل على قائمة كشوف
  2. لكلّ مسار موجود، توقّع موضعه في الإطار الحالي بمرشّح كالمان
  3. طابق الكشوف مع المسارات بخوارزمية هنغارية تُقلّل تكلفة إجمالية مبنيّة على IoU
  4. المسار غير المطابق يُتنبّأ به مرّة أخرى؛ إن بقي بلا مطابقة لبضعة إطارات (max_age)، يُحذف
  5. الكشف غير المطابق يُنشئ مسارًا جديدًا بمعرّف جديد

SORT بسيط، سريع (مئات الأجسام في الوقت الحقيقي)، وقاعدة صلبة. ضعفه: يُخطئ في المعرّفات حين تتقاطع مسارات (سيّارتان تعبران بعضهما)، لأنّه لا يعتمد إلّا على الموضع.

ByteTrack: استخدام الكشوف الضعيفة

ByteTrack (Zhang et al.، 2022) لاحظ أنّ الأنظمة الأخرى تُهمل الكشوف ذات الثقة المنخفضة، بينما هذه الكشوف غالبًا حقيقية لكن جسمها محجوب جزئيًا. فقد يكون كشف بثقة 0.3 لسيّارة مختبئة خلف شجيرة أفضل من إلغاء المسار.

ByteTrack يُطابق في مرحلتين:

  1. مطابقة أولى: بين المسارات والكشوف عالية الثقة (>0.6)
  2. مطابقة ثانية: بين المسارات غير المطابقة والكشوف منخفضة الثقة (0.1 إلى 0.6)

هذا يحفظ المسار خلال فترات الحجب دون قبول ضجيج كامل. النتيجة: تحسين ملحوظ في مقاييس MOTA وIDF1 مقارنة بـSORT، دون تعقيد حسابي إضافي كبير.

المقاييس المعيارية للتتبّع

اثنان أساسيان:

  • MOTA (Multi-Object Tracking Accuracy): يُعاقب الكشوف المفقودة، والإيجابيات الكاذبة، وتبديلات المعرّفات (ID switches)
  • IDF1 (ID F1 Score): درجة F1 للمعرّفات؛ يُقاس اتّساق تعيين المعرّفات عبر الوقت

نموذج بـMOTA عالٍ لكن IDF1 منخفض يعني كاشفًا دقيقًا لكن يخلط المعرّفات كثيرًا. عمليًا لعدّ السيّارات، IDF1 أهمّ: كلّ تبديل معرّف يعني احتساب سيّارة واحدة مرّتين.

عدّ عبر خطّ افتراضي

مرة ما بتنا نملك المسارات، العدّ يصير بسيطًا. نُعرِّف خطًّا افتراضيًا يعبر الصورة (مثلًا في منتصف التقاطع)، ونُحصي كلّما عبره مركز صندوق مسار. الشيفرة الجوهرية:

import numpy as np

def compte_par_ligne(ligne, tracks, historique_positions):
"""
ligne : (x1, y1, x2, y2) اطراف الخط
tracks : قاموس {track_id: (x_center, y_center)} في الاطار الحالي
historique_positions : قاموس {track_id: (x_prec, y_prec)}
يعيد عدد المسارات التي عبرت الخط في هذا الاطار
"""
def cote_du_ligne(p, ligne):
x, y = p
x1, y1, x2, y2 = ligne
# حاصل الضرب المتجهي: علامته تحدد الجانب
return np.sign((x2 - x1) * (y - y1) - (y2 - y1) * (x - x1))

count = 0
for tid, pos in tracks.items():
if tid not in historique_positions:
historique_positions[tid] = pos
continue

cote_avant = cote_du_ligne(historique_positions[tid], ligne)
cote_apres = cote_du_ligne(pos, ligne)

# عبر الخط اذا تغيرت العلامة
if cote_avant != 0 and cote_apres != 0 and cote_avant != cote_apres:
count += 1

historique_positions[tid] = pos

return count


# مثال باستخدام YOLOv8 + ByteTrack المدمج
from ultralytics import YOLO

model = YOLO("yolov8n.pt")

# tracker="bytetrack.yaml" يفعل ByteTrack مباشرة
results = model.track(
source="carrefour_video.mp4",
tracker="bytetrack.yaml",
persist=True, # يحافظ على المسارات بين الاطارات
classes=[0, 2, 5], # مشاة، سيارة، حافلة
)

ligne_compte = (100, 400, 900, 400) # افقي في منتصف الصورة
historique = {}
total = 0

for result in results:
if result.boxes.id is None:
continue
boxes = result.boxes.xywh.numpy() # (N, 4): x_center, y_center, w, h
ids = result.boxes.id.int().numpy()
tracks_courants = {int(i): (float(b[0]), float(b[1])) for i, b in zip(ids, boxes)}
total += compte_par_ligne(ligne_compte, tracks_courants, historique)

print(f"المركبات التي عبرت الخط: {total}")
تبديلات المعرّف تُضاعف العدّ

عدّ السيّارات ببساطة عبر «عدد المعرّفات الفريدة» خاطئ. سيّارة واحدة يمكن أن تكتسب معرّفين مختلفين إن مرّت خلف حافلة كبيرة (المسار الأصلي يُلغى بعد max_age، ثم يُنشأ مسار جديد لدى ظهور السيّارة مرّة أخرى). لهذا العدّ عبر خطّ افتراضي أوثق: إن عبرت السيّارة الخطّ مرّة واحدة، تُحسب مرّة واحدة، مهما تغيّر معرّفها بعد أو قبل.

في الخلاصة

  • التتبّع يربط كشوف الإطارات ببعضها لإنشاء مسارات ذات معرّف ثابت عبر الزمن.
  • مرشّح كالمان يتنبّأ بالموضع التالي ويُنعّم الصناديق، ما يجعله ركيزة كلّ الخوارزميات الحديثة.
  • SORT بسيط وفعّال بمطابقة IoU وخوارزمية هنغارية؛ ByteTrack يُحسّنه بمرحلة مطابقة ثانية للكشوف الضعيفة.
  • للعدّ العملي، الخطّ الافتراضي أوثق من عدّ المعرّفات، لأنّه يقاوم تبديلات المعرّف الناتجة عن الحجب.

الوحدة التالية: بناء مجموعة صور مخصّصة، توسيمها، تكثيرها، وكشف تسرّب البيانات الذي يُبطل كلّ تقييم.