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

دمج المهام في مسار واحد

أنشئ تسلسلًا للكشف ثم التصنيف، وصنّف كل كائن متعقب مرة واحدة بدلًا من تصنيفه في كل إطار، وحوّل النقاط المفتاحية للوضعية إلى سلاسل زمنية لكل شخص.

تتألف الأنظمة الأكثر فائدة بعد الكشف عادةً من نموذجين لا نموذج واحد. يكتشف كاشف كل عامل، ويحدد مصنّف ما إذا كان نوع خوذة كل عامل صحيحًا؛ ويكتشف نموذج الوضعية المفاصل، ثم يحوّلها متعقب إلى خط زمني يمكن إحصاء التكرارات منه. الكود قصير، لكن ما يعطّل هذه الأنظمة هو عدم تطابق مجموعة بيانات المرحلة الثانية مع القصاصات في المرحلة الأولى، والتكلفة التي تتضاعف مع كل كائن في كل إطار — فعند التشغيل الساذج، يكلف بث بمعدل 30 إطارًا في الثانية وعشرة أشخاص 300 عملية تصنيف في الثانية.

النتيجة

شغّل سكربتًا للكشف ثم التصنيف، ونسخةً تصنّف كل شخص متعقب مرة واحدة فقط، ومتعقبًا للوضعية يحفظ زوايا الركبة لكل شخص، وأنشئ بيانات تدريب المصنّف من قصاصات الكاشف نفسه.

المسار السريع
إذا كنت تعرف الأساسيات بالفعل، فإليك النسخة المختصرة.
  1. اكتشف، واقصص باستخدام result.orig_img[y1:y2, x1:x2]، وصنّف كل القصاصات في استدعاء واحد على دفعات.

  2. أنشئ مجموعة بيانات المصنّف من قصاصات الكاشف (save_crop=True)، لا من صور الاستوديو.

  3. في الفيديو، تتبّع باستخدام persist=True وصنّف كل result.boxes.id مرة واحدة، ثم أعد استخدام التسمية.

  4. عند الجمع بين الوضعية والتتبع، اربط سجل النقاط المفتاحية لكل شخص بمعرّف التتبع.

عملي

نمطا عمل يغطيان معظم المنتجات#

يبدأ كلا النمطين من الإطار نفسه ومن واجهة Ultralytics API نفسها؛ ويختلفان في ما تستهلكه المرحلة الثانية. أما اختيار المتعقب وضبطه ومعالجة التدفقات في تتبع الكائنات فتتناولها صفحة تتبّع الكائنات باستخدام ByteTrack وBoT-SORT وTrackTrack؛ ونركز هنا على ربط المهام.

اكتشف، ثم صنّف القصاصات#

from ultralytics import YOLO

detector = YOLO("yolo26n.pt")
classifier = YOLO("yolo26n-cls.pt")  # swap in your own fine-tuned classifier

result = detector("https://ultralytics.com/images/bus.jpg", conf=0.5)[0]
crops, labels = [], []
for box in result.boxes:
    x1, y1, x2, y2 = box.xyxy[0].int().tolist()
    crops.append(result.orig_img[y1:y2, x1:x2])  # BGR numpy crop, same as the detector saw
    labels.append(result.names[int(box.cls)])

for label, crop_result in zip(labels, classifier(crops, verbose=False)):  # one batched call for all crops
    probs = crop_result.probs
    print(f"{label:6s} -> {crop_result.names[probs.top1]} ({float(probs.top1conf):.2f})")
bus    -> bullet_train (0.31)
person -> sweatshirt (0.24)
person -> abaya (0.50)
person -> loudspeaker (0.49)
person -> frilled_lizard (0.24)

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

لا تتجاوز جودة المرحلة الثانية مدى ملاءمتها للقصاصات

لم يُدرَّب مصنّف ImageNet قط على أشخاص الشوارع الظاهرين في صور مقصوصة جزئيًا وبإطار ضيق، لذا فهو يخمّن — لاحظ أن الثقة لا تتجاوز 0.50. ويحدث الأمر نفسه لمصنّف مدرّب على صور منتجات واضحة ثم تُمرّر إليه قصاصات ضبابية من الكاشف.

أنشئ مجموعة بيانات المرحلة الثانية من قصاصاتك#

الطريقة الموثوقة للحصول على بيانات متطابقة هي أن تدع الكاشف ينشئها:

شبكة من قصاصات حقائب مرقمة حفظها Ultralytics YOLO26 من لقطات سير ناقل في مطار، جاهزة للفرز في مجلدات المصنّف

  1. شغّل الكاشف على لقطات ممثلة باستخدام save_crop=True، مثلًا yolo predict model=best.pt source=footage/ save_crop=True. تُحفظ القصاصات في runs/detect/predict/crops/<class>/.
  2. افرز قصاصات كل فئة في مجلداتها الدقيقة (helmet_ok/ وhelmet_missing/) — فهذا هو بالفعل تخطيط التصنيف الذي تناولناه في الدرس السابق.
  3. درّب المصنّف على تلك المجلدات، وتحقق من صحته باستخدام قصاصات من يوم أو كاميرا مختلفين.

بما أن الكاشف هو من اقتطع القصاصات، فإنها تحافظ على التأطير والتمويه والحجب الفعلي الذي ينتجه — وهو بالضبط ما سيراه المصنّف في بيئة الإنتاج — كما أن الضبط الدقيق عليها يُعد تعلمًا بالنقل اعتياديًا.

صنّف مرة واحدة لكل معرّف تتبع#

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

from ultralytics import YOLO

detector = YOLO("yolo26n.pt")
classifier = YOLO("yolo26n-cls.pt")  # swap in your own fine-tuned classifier
source = "https://github.com/ultralytics/assets/releases/download/v0.0.0/decelera_portrait_min.mov"

labels = {}  # track id -> (label, confidence), filled once per person
frames = boxes_seen = 0
for result in detector.track(source, classes=[0], stream=True, persist=True, verbose=False):  # people only
    frames += 1
    if result.boxes.id is None:
        continue
    for track_id, (x1, y1, x2, y2) in zip(result.boxes.id.int().tolist(), result.boxes.xyxy.int().tolist()):
        boxes_seen += 1
        if track_id not in labels:  # first sighting: classify once, then reuse
            probs = classifier(result.orig_img[y1:y2, x1:x2], verbose=False)[0].probs
            labels[track_id] = (classifier.names[probs.top1], round(float(probs.top1conf), 2))

print(f"{frames} frames, {boxes_seen} person boxes, {len(labels)} classifier calls")
print(labels)
11 frames, 33 person boxes, 3 classifier calls
{1: ('basketball', 0.51), 2: ('maillot_(tights)', 0.14), 3: ('bulletproof_vest', 0.23)}

ثلاث استدعاءات للمصنّف بدلًا من 33 في مقطع من 11 إطارًا؛ وفي بث طويل، تزداد الوفورات مع كل إطار يبقى فيه الشخص ظاهرًا. وما تزال التسميات مجرد تخمينات من ImageNet — استبدل المصنّف المضبوط بدقة، وستصبح الإجابة الخاصة بكل شخص.

قاعدة اتخاذ القرار

صنّف مرة واحدة لكل مسار عندما يتعذر تغير السمة أثناء ظهور الجسم (نوع الخوذة، طراز المركبة، رمز المنتج). وأعِد التصنيف كل N إطارًا عندما تكون السمة قابلة للتغير (باب مفتوح، يد مرفوعة). وإذا كانت المشاهدات الأولى صغيرة وضبابية عادةً، فصنّف الإطار الذي يكون فيه صندوق المسار أكبر ما يمكن.

الوضعية مع التتبع#

تقدير وضعية باستخدام Ultralytics YOLO لعدّ تمارين الضغط، مع هيكل الجسم وزاوية مرفق 110 درجات وعداد للتكرارات

يضيف وضع التتبع result.boxes.id مستمرًا لكل شخص، فتتحول النقاط المفتاحية إلى سلسلة زمنية. يسجّل هذا السكربت زاوية الركبة اليسرى لكل شخص عبر المقطع القصير نفسه:

from collections import defaultdict

import numpy as np

from ultralytics import YOLO

def angle(a, b, c):
    """Angle at b, in degrees, formed by points a-b-c."""
    ba, bc = a - b, c - b
    return np.degrees(np.arccos(np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))))

model = YOLO("yolo26n-pose.pt")
source = "https://github.com/ultralytics/assets/releases/download/v0.0.0/decelera_portrait_min.mov"
HIP, KNEE, ANKLE = 11, 13, 15  # left leg, COCO keypoint order
knee_angles = defaultdict(list)  # track id -> knee angle per frame

for result in model.track(source, stream=True, persist=True, verbose=False):
    if result.boxes.id is None:  # no confirmed tracks in this frame
        continue
    kpts = result.keypoints.cpu().numpy()
    for track_id, xy, conf in zip(result.boxes.id.int().tolist(), kpts.xy, kpts.conf):
        if conf[[HIP, KNEE, ANKLE]].min() > 0.5:  # skip occluded legs
            knee_angles[track_id].append(angle(xy[HIP], xy[KNEE], xy[ANKLE]))

for track_id, angles in sorted(knee_angles.items()):
    print(f"id {track_id}: {len(angles)} frames, left knee {min(angles):.0f}-{max(angles):.0f} deg")
id 1: 11 frames, left knee 170-176 deg
id 2: 11 frames, left knee 96-118 deg
id 3: 11 frames, left knee 177-180 deg

تطابق الركبة المثنية للشخص ذي المعرّف 2 وضعية الشخص الجالس في المقطع؛ أما الشخصان الآخران فيقفان بساقين مستقيمتين تقاربان 180°. ومن هنا، يكون عدّ التكرارات عملية عبور العتبات في سلسلة كل معرّف — نزولًا إلى ما دون زاوية، ثم صعودًا فوق زاوية أخرى — وهذا ما يوفّره لك حل مراقبة التمارين.

تقدير تكلفة سلسلة المعالجة#

عامل التكلفةالكشف ثم التصنيفالوضعية مع التتبع
يتناسب معالكائنات في كل إطارالإطارات
العامل الأهماقتطع دفعات من الصور؛ وصنّف مرة واحدة لكل معرّف مسار وخزّن النتيجة مؤقتًا.خفّض imgsz أو vid_stride عندما تكون الحركة بطيئة.
نمط الإخفاقيرى المصنّف اقتطاعات تختلف عن بيانات تدريبه.تؤدي تبديلات المعرّف إلى تقسيم سجل الشخص الواحد إلى جزأين.
تبديل المعرّفات يقطع التسلسلات

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

جرّبه

شغّل two_stage.py مع إضافة save_crop=True إلى استدعاء الكاشف، وتأكد من ظهور الاقتطاعات في مجلدات كل فئة. شغّل classify_per_track.py وقارن استدعاءات المصنّف بمربعات الأشخاص. ثم وسّع pose_track.py ليطبع، لكل معرّف مسار، عدد الإطارات التي تقل فيها زاوية الركبة عن 120°.

متى يُعدّ مكتملًا
تكون قد أتممت الدرس عند تحقق جميع الشروط التالية.
  • توجد الاقتطاعات ضمن crops/<class>/، ويمكنك وصف طريقة فرزها في مجلدات المصنّف.

  • يمكنك تحديد عدد استدعاءات المصنّف التي وفّرها التخزين المؤقت لكل مسار في المقطع التجريبي.

  • يطبع pose_track.py سطرًا واحدًا لكل معرّف مسار، يتضمن عدد إطاراته وعدد الإطارات التي تقل فيها الزاوية عن 120°.

عرض الحل

أضف save_crop=True إلى استدعاء الكاشف:

result = detector("https://ultralytics.com/images/bus.jpg", conf=0.5, save_crop=True)[0]

تظهر الاقتطاعات ضمن runs/detect/predict*/crops/person/ وcrops/bus/ (يزداد رقم مجلد التشغيل مع كل عملية تنبؤ). أتاح التخزين المؤقت لكل مسار إجراء 3 استدعاءات للمصنّف لـ 33 مربعًا للأشخاص، أي توفير 30 استدعاءً في مقطع من 11 إطارًا.

لحساب عدد زوايا الركبة، استبدل الحلقة الأخيرة في pose_track.py:

for track_id, angles in sorted(knee_angles.items()):
    bent = sum(a < 120 for a in angles)
    print(f"id {track_id}: {len(angles)} frames, {bent} with knee below 120 deg")
id 1: 11 frames, 0 with knee below 120 deg
id 2: 11 frames, 11 with knee below 120 deg
id 3: 11 frames, 0 with knee below 120 deg

الشخص الجالس ذو المعرّف 2 لديه زاوية تقل عن 120° في الإطارات الـ11 جميعها؛ أما المعرّفان 1 و3 فلا يحدث ذلك معهما مطلقًا.

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

أخيرًا، نقيّم كل مهمة باستخدام المقياس المناسب ونصدّر كل نموذج منها للنشر.

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