الوحدة 2 — الكشف ذو المرحلتين: عائلة R-CNN
انتهت الوحدة السابقة إلى نتيجة عمليّة: مشروع عدّ السيّارات على التقاطع يتطلّب كشفًا، لا تصنيفًا. تبقى مسألة كيف ننفّذ الكشف. تعرض هذه الوحدة أوّل مقاربة نجحت فعلًا — عائلة R-CNN — التي تفصل المشكلة إلى مرحلتين: أوّلًا نقترح مناطق محتملة، ثم نصنّفها ونصحّح موضعها.
من النافذة المنزلقة إلى الاقتراحات
المقاربة الساذجة قبل R-CNN كانت النافذة المنزلقة: نمرّر مصنّف صور على كلّ موضع وكلّ مقياس وكلّ نسبة عرض/ارتفاع ممكنة، ونحتفظ بالمواضع التي يقول عنها المصن ّف «سيّارة». لكنّ هذا كارثي حسابيًا: على صورة 500 في 500 بكسل، بعشرات الأحجام والنسب، تصير النوافذ المُقيَّمة بمئات الآلاف. وعلى كامل مجموعة تدريب، النتيجة هي أسابيع من الحساب لصورة واحدة.
طرح R-CNN الأصلي (Girshick، 2014) خطوة وسيطة: خوارزمية Selective Search تُنتج قرابة 2000 «اقتراح» فقط لكلّ صورة، وهي نوافذ من المرجَّح أن تحتوي على جسم. ثم تُمرَّر كلّ نافذة عبر شبكة CNN لاستخراج متّجه سماتها، ويُصنَّف بمصنّف SVM لكلّ صنف. مكسب هائل مقارنة بالنافذة المنزلقة، لكن السرعة تبقى بضع دقائق للصورة الواحدة، لأنّ كلّ اقتراح يتطلّب مرورًا كاملًا في CNN.
Fast R-CNN: مشاركة الحساب
جاء Fast R-CNN (Girshick، 2015) بفكرة بسيطة وعميقة: لا نُمرّر الشبكة على كلّ اقتراح على حدة. بل نُمرّرها مرّة واحدة على الصورة كلّها لإنتاج خريطة سمات، ثم نستخرج من هذه الخريطة الجزء المقابل لكلّ اقتراح.
المشكلة التقنية: الاقتراحات لها أحجام مختلفة، لكنّ الطبقات الكاملة الاتّصال في نهاية الشبكة تحتاج إلى دخل ثابت الحجم. حلّ Fast R-CNN هو RoI Pooling (Region of Interest Pooling): تقسيم منطقة الاقتراح داخل خريطة السمات إلى شبكة ثابتة مثل 7×7، وأخذ الحدّ الأقصى داخل كلّ خليّة.
هذا يُعطي متّجه سمات بحجم موحّد لأيّ اقتراح، ويسمح بالتدريب المشترك للمصنّف وللانحدار الذي يُصحّح إحداثيات الصندوق. ورأس الشبكة يُنتج مُخرَجين متوازيين: احتمالات الأصناف، وإزاحة الصندوق نسبةً إلى الاقتراح. الخسارة الكلّية هي مجموع خسارة إنتروبيا متقاطعة للتصنيف وخسارة L1 مُنعّمة لتصحيح الصندوق.
Faster R-CNN: شبكة اقتراح المناطق
بقي Fast R-CNN معتمدًا على Selective Search الخارجية والبطيئة. جاء Faster R-CNN (Ren et al.، 2015) ليغلق الحلقة: شبكة اقتراح المناطق (Region Proposal Network، اختصارًا RPN) تعمل على خريطة السمات نفسها وتُنتج الاقتراحات مباشرة.
الفكرة: على كلّ نقطة من خريطة السمات، نُعرِّف مجموعة من المراسي (anchors) بأحجام ونسب مختلفة (مثلًا 3 مقاييس × 3 نسب = 9 مراسي لكلّ نقطة). ثم RPN تُنتج لكلّ مرساة تنبّؤين: احتمال أنّها تحتوي على جسم (بلا تحديد الصنف)، وتصحيحات لإحداثياتها. نحتفظ بأعلى مرساة موصى بها، ونمرّرها إلى المرحلة الثانية لتصنيفها الفعلي.
النتيجة معمارية مُتّحدة كليًّا، قابلة للتدريب من طرف إلى طرف، وأسرع بأمر مقدار من Fast R-CNN.
RoI Align: تصحيح انحياز عدم المحاذاة
عيب RoI Pooling أنّها تقرّب إحداثيات الاقتراح إلى أقرب خليّة، ثم تقرّب حدود كلّ خليّة داخلية. هذان التقريبان يزيحان الميزة عن موضعها الفعلي بنصف بكسل تقريبًا في كلّ اتّجاه، وهو ما يُدهور التقطيع الدقيق (سنعود إليه في الوحدة 7).
جاء RoI Align (He et al.، 2017، في ورقة Mask R-CNN) لحلّ هذا: لا تقريب، بل استيفاء ثنائي الخطّية لقيم البكسلات في مواضع حقيقية عشرية. يُحسّن دقّة الصناديق قليلًا، ويُحسّن دقّة أقنعة النُسَخ بأمر مقدار.
استخدام Faster R-CNN بـtorchvision
torchvision يُقدّم Faster R-CNN مُدرَّبًا مسبقًا على COCO. لتطبيقه على صورة من كاميرا التقاطع:
import torch
import torchvision
from torchvision.transforms import functional as F
from PIL import Image
# النموذج المُدرَّب مسبقا على COCO
weights = torchvision.models.detection.FasterRCNN_ResNet50_FPN_Weights.DEFAULT
model = torchvision.models.detection.fasterrcnn_resnet50_fpn(weights=weights)
model.eval()
# صورة من كاميرا التقاطع
image = Image.open("carrefour_10h32.jpg").convert("RGB")
tensor = F.to_tensor(image)
with torch.no_grad():
outputs = model([tensor])
# مخرج لصورة واحدة
predictions = outputs[0]
boxes = predictions["boxes"] # (N, 4) بصيغة (x1, y1, x2, y2)
labels = predictions["labels"] # (N,) معرفات اصناف COCO
scores = predictions["scores"] # (N,) بين 0 و 1
# نحتفظ بالسيارة (3) والمشاة (1) والحافلة (6) فقط
categories_of_interest = {1: "مشاة", 3: "سيارة", 6: "حافلة"}
mask = torch.isin(labels, torch.tensor(list(categories_of_interest.keys())))
mask &= scores > 0.5
filtered_boxes = boxes[mask]
filtered_labels = [categories_of_interest[int(l)] for l in labels[mask]]
print(f"عدد الاجسام: {len(filtered_boxes)}")