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

الوحدة 10 — مشروع: كشف على مجموعة صور مخصّصة

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

المجموعة: بنية الملفّات

نبدأ ببنية ملفّات معيارية لـYOLO. مجموعة carrefour_v1 تتضمّن 3000 صورة مصنّفة إلى ثلاث حالات مرور:

datasets/carrefour_v1/
├── images/
│ ├── train/ (2100 صورة، مصورة من الاثنين للجمعة، الساعة 7-11)
│ ├── val/ (600 صورة، مصورة السبت 7-11)
│ └── test/ (300 صورة، مصورة الأحد 7-11)
├── labels/
│ ├── train/ (ملف .txt لكل صورة، صيغة YOLO)
│ ├── val/
│ └── test/
└── data.yaml

التقسيم بالأيّام يحترم قاعدة الوحدة السابقة: لا صور متقاربة زمنيًا بين المجموعات. ملفّ data.yaml:

path: datasets/carrefour_v1
train: images/train
val: images/val
test: images/test

names:
0: pedestrian
1: bicycle
2: car
3: motorcycle
4: bus
5: truck

الضبط الدقيق لـYOLOv8

انطلاقًا من الأوزان المُدرَّبة مسبقًا على COCO (التي تعرف السيّارة والمُشاة والحافلة سلفًا)، الضبط الدقيق يحتاج غالبًا 30 إلى 100 حقبة على مجموعة كهذه:

from ultralytics import YOLO

# انطلق من yolov8s (small): توازن جيد للضبط الدقيق
model = YOLO("yolov8s.pt")

results = model.train(
data="datasets/carrefour_v1/data.yaml",
epochs=60,
imgsz=640,
batch=16,
lr0=0.01, # معدل التعلم الاولي
lrf=0.01, # النسبة النهائية = lr0 * lrf
momentum=0.937,
weight_decay=0.0005,
warmup_epochs=3, # الوحدة 5 من دورة اساسيات التعلم العميق
box=7.5, # وزن خسارة الصندوق
cls=0.5, # وزن خسارة الصنف
dfl=1.5, # وزن خسارة التوزيع
# تكثير مدمج في YOLOv8
mosaic=1.0, # Mosaic دائما في التدريب
close_mosaic=10, # نطفئ Mosaic في اخر 10 حقب للاستقرار
hsv_h=0.015, # اضطراب HSV
hsv_s=0.7,
hsv_v=0.4,
flipud=0.0, # لا انقلاب عمودي (السماء تبقى فوق)
fliplr=0.5,
project="runs/carrefour",
name="v1_yolov8s",
device=0, # GPU 0
)

نقاط تصميم تستحقّ الشرح:

  • flipud=0.0: لا نُقلب الصورة رأسًا على عقب. للتصنيف العامّ ذلك مفيد، لكن للكاميرا الثابتة على تقاطع طرق سماؤها فوق وطريقها تحت، الانقلاب العمودي يُدخل بيانات لن نراها أبدًا في الإنتاج.
  • close_mosaic=10: Mosaic يجمع أربع صور في واحدة لتنويع الأحجام، لكنّه يُشوّه الإحصائيات. تعطيله في آخر عشر حقب يسمح للنموذج بالاستقرار على بيانات «نظيفة» قبل النشر.
  • warmup_epochs=3: تسخين معدّل التعلّم في الحقب الأولى، كما رأينا في أساسيات التعلّم العميق.

قراءة التقرير الأوّل

بعد التدريب، YOLOv8 يُنتج تقريرًا مُفصّلًا. مثال واقعي:

Class      Images  Instances    Box(P     R    mAP50  mAP50-95)
all 600 4832 0.822 0.741 0.798 0.542
pedestrian 600 1204 0.751 0.628 0.702 0.412
bicycle 600 342 0.694 0.512 0.601 0.354
car 600 2547 0.911 0.856 0.891 0.647
motorcycle 600 218 0.723 0.601 0.688 0.398
bus 600 302 0.876 0.795 0.845 0.612
truck 600 419 0.887 0.812 0.861 0.628

قراءة سطر بسطر تُنبّه إلى الأولويات:

  • الأداء على car ممتاز (mAP50-95 = 0.647): آلاف الأمثلة، ونموذج مسبق يعرفها من COCO
  • bicycle (0.354) وmotorcycle (0.398) الأضعف: عدد الأمثلة صغير (342 و218)
  • pedestrian (0.412) أقلّ ممّا كنّا نأمل: نحتاج إلى فحص الأخطاء

تحليل الأخطاء بحسب الصنف وحجم الجسم

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

# التقييم على مجموعة الاختبار مع تفصيل حسب الحجم
metrics = model.val(
data="datasets/carrefour_v1/data.yaml",
split="test",
save_json=True, # تقرير COCO الكامل
plots=True, # ينتج curves ومصفوفة التباس
)

# مقاييس بحسب حجم الجسم (small < 32^2 بكسل، medium < 96^2، large باقي)
print(f"mAP على الأجسام الصغيرة: {metrics.box.maps_small.mean():.3f}")
print(f"mAP على الأجسام المتوسطة: {metrics.box.maps_medium.mean():.3f}")
print(f"mAP على الأجسام الكبيرة: {metrics.box.maps_large.mean():.3f}")

نتائج نموذجية:

الحجمmAP50-95
صغير (المُشاة البعيد، الدرّاجة الخلفية)0.291
متوسّط (السيّارة العادية)0.612
كبير (الحافلة، الشاحنة)0.741

الفارق كبير على الصغيرة، وهو ما يفسّر جزءًا من ضعف pedestrian (كثير من المُشاة البعيد صغير). ثلاث إجراءات ممكنة:

  1. دقّة دخل أعلى: imgsz=1280 بدل 640 يُضاعف عدد البكسلات على كلّ جسم صغير، لكن يُضاعف كلفة الحساب أيضًا
  2. تكثير مُركَّز على الحجم: A.RandomScale(scale_limit=0.5) مع Albumentations يُنشئ نسخًا مُصغَّرة من أجسام كبيرة
  3. جمع بيانات إضافية: صور خاصّة بالأجسام البعيدة، مصوّرة بزوايا مختلفة

مصفوفة الالتباس: من يُخلط بمن؟

المصفوفة الناتجة تكشف خلطات محدّدة. لعدّ التقاطع النموذجي:

truckcarbusmotorcyclebicyclepedestrianbackground
truck871230008
car159235120041
bus52710003
motorcycle0806221315
bicycle00018461222
pedestrian0004825361

خلطة bicycle → motorcycle (18 حالة) وخلطة motorcycle → bicycle (21 حالة) واضحة. الحلّ ليس نموذجًا أعقد، بل تعريف أوضح في دليل التوسيم: هل الدرّاجة الكهربائية درّاجة أم دراجة نارية؟ وربّما دمج الصنفين في «مركبة ذات عجلتين» إن كان التمييز غير ضروري للاستخدام النهائي.

التصدير للنشر

بعد التحقّق من الأداء، نُصدّر النموذج بصيغة قابلة للنشر خارج بايثون:

# تصدير الى ONNX للنشر عبر الاجهزة
model.export(format="onnx", imgsz=640, simplify=True, opset=17)
# ينتج: runs/carrefour/v1_yolov8s/weights/best.onnx

# تصدير الى TensorRT للاستدلال المسرع على GPU NVIDIA
model.export(format="engine", imgsz=640, half=True) # FP16
# ينتج: best.engine

# تصدير الى CoreML لأجهزة Apple
model.export(format="coreml", imgsz=640)

# اختبار سريع للنموذج المصدر
from ultralytics import YOLO
onnx_model = YOLO("runs/carrefour/v1_yolov8s/weights/best.onnx")
results = onnx_model("test.jpg")

اختيار الصيغة يعتمد على النشر:

  • ONNX: عامّ ومحمول، يعمل مع ONNX Runtime على CPU وGPU وموبايل
  • TensorRT: أسرع خيار على GPU NVIDIA، لكنّه مرتبط بالمنصّة
  • CoreML: أجهزة Apple
  • TFLite: أجهزة موبايل ذات موارد محدودة

تكامل مع سلسلة التتبّع

الخطوة الأخيرة: ربط الكاشف المُدرَّب بـByteTrack من الوحدة 8، والعدّ عبر خطّ افتراضي:

from ultralytics import YOLO

model = YOLO("runs/carrefour/v1_yolov8s/weights/best.pt")

results = model.track(
source="video_test_dimanche.mp4",
tracker="bytetrack.yaml",
conf=0.5,
iou=0.5,
persist=True,
save=True, # يحفظ فيديو التقييم
)

# نجمع الاحصاءات
compte_par_classe = {name: 0 for name in model.names.values()}
historique_positions = {}
ligne = (0, 400, 1280, 400) # افقي في منتصف الاطار

for r in results:
if r.boxes.id is None:
continue
for box, tid, cls in zip(r.boxes.xywh, r.boxes.id.int(), r.boxes.cls.int()):
pos = (float(box[0]), float(box[1]))
# نطبق منطق العد من الوحدة 8
...

print(compte_par_classe)
# {'pedestrian': 187, 'car': 542, 'bus': 12, 'truck': 23, ...}
دورة تحسين واقعية: بيانات ثم معمارية

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

في الخلاصة

  • الضبط الدقيق لـYOLOv8 على مجموعة مخصّصة يبدأ من أوزان COCO، بـ30 إلى 100 حقبة، مع تكثير محدود يحترم فيزياء الكاميرا (لا انقلاب عمودي).
  • تحليل الأخطاء يبدأ بجدول mAP لكلّ صنف، ثم بالتبعيض حسب حجم الجسم، ثم بمصفوفة الالتباس التي تكشف الخلطات المحدّدة.
  • الأجسام الصغيرة أضعف بأمر مقدار؛ ثلاث روافع: دقّة دخل أعلى، تكثير مركّز على المقياس، بيانات إضافية.
  • التصدير يختار حسب النشر (ONNX عامّ، TensorRT لـNVIDIA، CoreML لـApple)، ويُختبر دائمًا على مجموعة الاختبار قبل الإنتاج.

الخطوة الأخيرة: مراجعة كاملة للدورة وامتحان الأربعين سؤالًا.