المحتويات
- مقدمة
- الفرق بين تتبع الكائنات والكشف عن الكائنات
- المبدأ العام لعمل خوارزمية تتبع الكائنات
- تحقيق خوارزمية تتبع الكائنات
- تطبيق عملي لتتبع الوجه باستخدام خوارزمية تتبع الكائنات
- فيديو يظهر خرج التطبيق العملي
- الخاتمة
- المصادر
مقدمة
تعد خوارزمية تتبع الكائنات واحدة من أهم الخوارزميات المستخدمة في مجال الرؤية الحاسوبية والتي تهدف بشكل عام إلى تتبع الكائنات منذ بداية ظهورها في الفيديو أو على عدسة الكاميرا لحين اختفائها وبالتالي إمكانية تسجيل معلومات حول هذه الكائنات ومعرفة سلوكها ، على سبيل المثال في حال المراقبة المرورية تمكننا هذه الخوارزمية من تتبع المركبات ومعرفة إذا كانت قد خالفت قانون السير أم لا (تجاوز السرعة المحددة ، تجاوز إشارة المرور) ،وبالاضافة الى ذلك يوجد تطبيقات كثيرة ومتنوعة لهذه الخوارزمية منها المراقبة الأمنية (ملاحقة المجرمين) و السيارة ذاتية القيادة والتصوير الطبي .
من الناحية التقنية يبدأ تتبع الكائنات باكتشافها أولاً أي تحديد موضع هذه الكائنات في الصورة عن طريق وضع مربعات محيطة بها Bounding box ، ثم تقوم خوارزمية التتبع بتعيين رقم معرف ID خاص بكل كائن ،لتبدأ عندها عملية التتبع لكل كائن تم تعيين رقم معرف له .
يتطلب إجراء عملية تتبع الكائنات في البداية كما ذكرنا سابقاً تحديد موضع الكائنات في الصورة لذلك يجب استخدام خوارزمية الكشفُ عن الكائنات Object Detection بشكل مرافق لخوارزمية تتبع الكائنات .
هناك نوعين رئيسيين لخوارزمية التتبع:
- تتبع الكائنات في مقطع فيديو : يتم تطبيق خوارزمية تتبع الكائنات في هذا النوع على فيديو مسجل مسبقاً حيث يتم اكتشاف الكائنات الموجودة في إطارات الفيديو وتتبع انتقالها عبر هذه الإطارات.
- تتبع الكائنات في الزمن الحقيقي : يتم اكتشاف الكائنات وتتبعها مباشرة وفي الزمن الحقيقي عن طريق معالجة الإطارات المتدفقة من كاميرا المراقبة ،حيث هذا النوع يمثل تحدياً أكبر فهو يتطلب موارد حاسوبية قوية تساعد على المعالجة في الزمن الحقيقي دون حدوث تأخير زمني .
الفرق بين تتبع الكائنات والكشف عن الكائنات
تتلخص مهمة خوارزمية الكشف عن الكائنات بشكل عام في اكتشاف جميع الكائنات الموجودة في الصورة بالاضافة الى تسمية هذه الكائنات (أي تحديد نوعها سيارة – شخص – قطة . . ) حيث عملية الكشف عن كائن تتم بتحديد موضع هذا الكائن ووضع مربع إحاطة حوله ومعرفة نوعه ايضاً ،أما بالنسبة لخوارزمية تتبع الكائنات فهي تعتمد بشكل أساسي في عملها على خوارزمية الكشف عن الكائنات وذلك لأن إجراء عملية التتبع لكائن يتطلب في البداية الكشف عن هذا الكائن وتحديد موضعه وبمجرد الكشف عنه تبدأ عملية التتبع له طالما هو في حالة ظهور وتنتهي هذه العملية عند اختفاء الكائن .
المبدأ العام لعمل خوارزمية تتبع الكائنات
بفرض لدينا مقطع فيديو مسجل ونريد تطبيق خوارزمية التتبع عليه ،بالتالي تعمل هذه الخوارزمية بالشكل التالي :
أولاً :
تتم قراءة الإطار الأول من الفيديو ويطبق عليه خوارزمية الكشف عن الكائنات وحساب مركز كل كائن(أي مركز مربع الإحاطة Bounding box ) ثم يتم إعطاء معرف ID خاص بكل كائن ،الآن بالنظر إلى الشكل 2 يتوضح لدينا وجود كائنين في الإطار الأول الكائن ذو المعرف 1 و الكائن ذو المعرف 2 .
ثانياً :
تُطَبّق خوارزمية الكشف عن الكائنات على الإطار الثاني من الفيديو
- عزيزي القارئ لابدّ أن تعرف أن الكائنات تتحرك خلال الفيديو بالتالي تتغير مواضع هذه الكائنات بالنسبة لإطارات الفيديو.
- بالنظر إلى الشكل 3 نلاحظ أن النقاط التي باللون الأرجواني تمثل المواضع القديمة للكائنات أي في الإطار الأول.
- النقاط الصفراء تمثل المواضع الجديدة للكائنات أي في الإطار الثاني حيث نلاحظ أنه يوجد 3 كائنات بالتالي ظهر لدينا كائن جديد في هذا الإطار.
- يتم حساب المسافة بين موضع كل كائن من الإطار الأول مع مواضع الكائنات في الإطار الثاني ،فمثلا بالنسبة للكائن ذو المعرّف 1 في الإطار الأول يتم حساب المسافة بينه وبين الكائنات الثلاثة المكتشفة في الإطار الثاني (المستقيمات الحمراء ) وكذلك بالنسبة للكائن ذو المعرف 2 (المستقيمات الخضراء ).
ثالثاً :
نلاحظ أن الكائنات الثلاثة الموجودة في الإطار الثاني الموضحة في الشكل-4 غير معروف بأي كائن هي مرتبطة في الإطار الأول لذلك تتم العملية التالية كما يلي :
- البحث عن أصغر مسافة بين كل كائن من الكائنات الموجودة في الإطار الأول مع الكائنات الموجودة في الإطار الثاني.
- بالنسبة للكائن ذو المعرّف 1 المسافة بينه وبين الكائن الذي يعلوه أصغر ما يمكن بالتالي يعتبر الكائن الذي يعلوه مرتبط به أي هو نفسه ويتم تعريفه وإعطاؤه نفس المعرف أي 1.
- بالنسبة للكائن ذو المعرف 2 المسافة بينه وبين الكائن الذي يعلوه أيضا أصغر ما يمكن بالتالي يتم إعطاؤه نفس المعرف آي 2.
- بالنسبة للكائن المتبقي (أسفل اليسار) لم يتم مطابقته مع الكائنات الموجودة في الإطار الأول بالتالي من المؤكد أنه كائن جديد ظهر في الإطار الثاني بالتالي يتم تعريفه وإعطاؤه معرف جديد هو 3.
رابعاً :
- بعد القيام بتحديد الكائن الجديد كما هو موضح في الشكل 5 يتم إضافته الى قائمة الكائنات التي يجري تتبعها ،بعد ذلك يتم تكرار الخطوات السابقة من 2 إلى 4 من أجل كل إطار من إطارات الفيديو .
- من أجل كل كائن متتبع من الممكن عند إطار معين أن يختفي ظهوره ,بالتالي في هذه الحالة يجب حذف هذا الكائن من قائمة الكائنات المتتبعة مع العلم أنه يوجد عتبة N من خلالها يمكن تحديد إن كان الكائن قد انتهى ظهوره أم لا ولتوضيح الفكرة أكثر سنأخذ المثال التالي :
بفرض لدينا قيمة N=50 حيث الكائن بلحظة زمنية معينة اختفى بالتالي بعد مرور 50 إطار إذا لم يظهر الكائن في إطارات الفيديو بالتالي يتم إلغاء تتبعه من خلال حذفه من قائمة الكائنات المتتبعة .
تحقيق خوارزمية تتبع الكائنات
قبل البدء بتحقيق خوارزمية تتبع الكائنات لابد من توضيح بعض المعاملات المهمة التي تتعامل معها هذه الخوارزمية :
أولاً – الاختفاء الأعظمي Max_disappear
يعبر عن العدد الأعظمي للإطارات التي يمكن من خلالها معرفة إن كان الكائن المتتبع قد اختفى بالفعل أم لا .
لنقوم بتوضيح أهمية هذا البارامتر من خلال المثال التالي :
بفرض في أول إطار من الفيديو ظهر كائن بالتالي تبدأ عملية التتبع لهذا الكائن ابتداءً من الإطار الثاني, مع العلم أن هذا الكائن موجود في كامل الفيديو وبفرض حدث ضجيج أدى إلى ظهور كامل الإطار الرابع والخامس والسادس باللون الاسود وبالتالي عند قراءة خوارزمية التتبع لهذه الإطارات ستظن أن الكائن قد اختفى وتلغي تتبعه ولكن بالحقيقة هو موجود وهذه مشكلة كبيرة.
يتم حل المشكلة السابقة من خلال معامل الاختفاء الأعظمي فمثلاً إذا كانت قيمته 10 فعندها خوارزمية التتبع تنتظر بعد قراءة 10 اطارات متتالية إذا لم يظهر هذا الكائن بالتالي هذا الكائن قد اختفى من الفيديو .
في المثال السابق عندما لم يظهر الكائن في الإطار الرابع والخامس والسادس بالشكل الصحيح عندها خوارزمية التتبع تنتظر ولا تقم بإلغاء تتبعه , فعند قراءة الإطار السابع تجد ان هذا الكائن التي كانت تتبعه قد ظهر مجدداً بالتالي تتابع عملية التتبع لهذا الكائن وكأنه لم يحدث أي خطأ .
ثانياً – المسافة العظمى Max_distance
أقصى مسافة مستخدمة للربط بين الكائنات أثناء عملية التتبع.
فعلى سبيل المثال :
- إذا كانت قيمة المسافة العظمى هي 175 بكسل ويوجد على سبيل المثال ثلاث كائنات تبتعد عن الكائن ذو المعرف 1 بالمسافات التالية 40 , 130 , 250 .
- بالتالي يتم الوضع بالحسبان الكائنات التي تبتعد عن الكائن ذو المعرّف 1 بمسافة أقل من 175 أي الكائنات التي تبتعد بمسافة 40 , 130 .
- بعدها يتم اختيار الكائن الذي يحقق أقصر مسافة عن الكائن ذو المعرف 1 أي الكائن الذي يبعد بمسافة 40 بالتالي هو الكائن نفسه .
- الكائن الذي يبتعد بمسافة 250 لا يتم معالجته نهائياً ولايتم وضعه بالحسبان بالنسبة للكائن ذو المعرف 1 لأنه يبتعد عنه بمسافة أكبر من 175 .
إنّ تضمين معامل المسافة العظمى والتعامل معه ضمن خوارزمية التتبع أمر مهم جداً ،و لتوضيح أهمية هذا المعامل نأخذ المثال التالي :
نفرض أنه لم يتم التعامل مع هذا المعامل ضمن الخوارزمية أي لا يوجد مسافة قصوى للربط بين الكائنات بالتالي الكائن يمكن أن يرتبط مع أي كائن يبتعد عنه بأي مسافة كانت ،وهنا تظهر لدينا مشكلة سنطرحها كالتالي :
- بفرض كان لدينا كائن أول يظهر في إطارات الفيديو ثم انتهى ظهور هذا الكائن في الإطار العاشر على سبيل المثال.
- في نفس هذا الإطار (الإطار العاشر) ظهر كائن جديد ثاني مختلف تماما عن الكائن الأول.
- لكن خوارزمية التتبع مازالت تتبع الكائن الأول ولنفرض موضع الكائن الثاني يبتعد عن موضع الكائن الأول بمسافة 300 مثلاً .
- بالتالي خوارزمية التتبع ستربط الكائن الأول بالكائن الثاني وتعتبره نفسه وذلك لأننا افترضنا أن الخوارزمية لا تتعامل مع معامل المسافة العظمى وهذه مشكلة كبيرة ومن هنا ندرك أهمية هذا البارامتر .
ملاحظة هامة : يتم تتبع الكائن من خلال إحداثيات مركز هذا الكائن centroid أي عندما نذكر أننا نقوم بحساب المسافة بين كائنين نقصد أننا نحسب المسافة الاقليدية بين مركزي هذين الكائنين (x1,y1) و (x2,y2) .
الأن نقوم بتحقيق خوارازمية التتبع حيث سنوضح الشفرة البرمجية الخاصة بصف تتبع الكائن CentroidTracker:
في البداية نقوم باستيراد المكتبات والتوابع التي سنتعامل معها
نقوم باستيراد تابع المسافة distance من مكتبةُ الحَوسَبةِ العلميَّةِ scipy لاستخدامه في حساب المسافات الاقليدية .
إن استيراد القاموس المرتب OrderedDict من المكتبة كولكشن collections والذي يحافظ على ترتيب عناصر القاموس بالطريقة التي تم إدخالها بعكس القاموس العادي Dictionary الذي لا يأخذ الترتيب بالحسبان .
تعريف الصف CentroidTracker يحتوي على باني يأخذ معاملين دخل هما maxDissapeared و maxDistance حيث تم توضيح هذه المعاملات سابقاً ،يتم وضع قيمة إفتراضية 50 لكلا المعاملين.
إن الصف CentroidTracker له الواصفات التالية :
- nextObjectID : يعبر عن المعرف ID التي تعطيها الخوارزمية للكائن .
- Objects : قاموس الكائنات المتتبعة يحوي جميع الكائنات التي يتم تتبعها وهو من نوع القواميس المرتبة حيث كل عنصر في هذا القاموس يتألف من مفتاح key وقيمة value ،المفتاح يعبر عن المعرف الخاص بالكائن أما القيمة تعبر عن مركز الكائن (x,y) .
- disappeared : قاموس الكائنات المخفية وهو من نوع القواميس المرتبة حيث المفتاح يعبر عن المعرف الخاص بالكائن أما القيمة تمثل عدد الإطارات المتتالية التي اختفى فيها الكائن المتتبع .
- maxDissapeared : الاختفاء الأعظمي إما يتم تمرير قيمتها أثناء إنشاء غرض من الصف أوتأخذ القيمة الافتراضية والتي هي 50.
- maxDistance : المسافة العظمى إما أن يتم تمرير قيمتها أثناء انشاء غرض من الصف أو تأخذ القيمة الإفتراضية والتي هي 50.
الشيفرة البرمجية :
from scipy.spatial import distance as dist
from collections import OrderedDict
import numpy as np
class CentroidTracker:
def __init__(self, maxDisappeared=50, maxDistance=50):
self.nextObjectID = 0
self.objects = OrderedDict()
self.disappeared = OrderedDict()
self.maxDisappeared = maxDisappeared
self.maxDistance = maxDistance
والآن سنعرف طريقة تسجيل كائن register التي تأخذ وسيط centroid يمثل مركز الكائن ،إن مهمة هذه الطريقة هي تسجيل كائن جديد ليجري تتبعه حيث يتم إعطاء رقم معرف للكائن وإضافة مركزه الى قاموس الكائنات المتتبعة وبالإضافة إلى تهيئة قيمة الـ disappear الخاصة بهذا الكائن بالقيمة صفر ،في النهاية نقوم بزيادة قيمة الـ nextObjectID بمقدار واحد من أجل إعطاء معرف للكائن الذي سيتم تسجيله بعد هذا الكائن.
الشيفرة البرمجية :
# Register an object
def register(self, centroid):
self.objects[self.nextObjectID] = centroid
self.disappeared[self.nextObjectID] = 0
self.nextObjectID += 1
نُعرف طريقة إلغاء تسجيل كائن deregister تأخذ وسيطاً objectID يمثل معرف الكائن المراد حذفه وإلغاء تتبعه ،تتمثل مهمة هذه الطريقة بإلغاء تتبع كائن عن طريق حذف بياناته (أي مركز الكائن وقيمة الاختفاء الخاصة به) من القاموسين قاموس الكائنات المتتبعة وقاموس الكائنات المخفية.
الشيفرة البرمجية :
# Deregister an object
def deregister(self, objectID):
del self.objects[objectID]
del self.disappeared[objectID]
عزيزي القارئ الآن سنكتب طريقة التحديث update التي تأخذ معامل rects وهو عبارة عن قائمة تحتوي على احداثيات مربعات الإحاطة للكائنات التي تم اكتشافها وذلك من أجل إجراء تتبع لهذه الكائنات .
نلاحظ أن التعليمة الشرطية في حال كان طول القائمة يساوي صفر (أي لم يتم اكتشاف أي كائن) ستكون جميع الكائنات المتتبعة في حالة اختفاء disappeared .
و الحلقة التكرارية من أجل المرور على جميع المفاتيح من قاموس الكائنات المختفية ليتم زيادة كل قيم هذا القاموس بمقدار واحد.
في حال أصبحت قيمة الاختفاء لكائن ما أكبر من الاختفاء الأعظمي بالتالي يتم إلغاء تتبع هذا الكائن وحذفه بواسطة طريقة الغاء تسجيل كائن .
في النهاية يتم إرجاع قاموس الكائنات المتتبعة بعد إجراء التحديث عليه.
الشيفرة البرمجية :
# Update the tracker
def update(self, rects):
if len(rects) == 0:
for objectID in list(self.disappeared.keys()):
self.disappeared[objectID] += 1
if self.disappeared[objectID] > self.maxDisappeared:
self.deregister(objectID)
return self.objects
نقوم بتعريف قائمة مهيأة بأصفار inputCentroids والتي هي قائمة مراكز الكائنات الموجودة في الإطار الحالي حيث يتم إسناد مراكز الكائنات إليها فيما بعد ،وذلك بالاعتماد على قيم القائمة التي تحتوي على احداثيات مربعات الإحاطة للكائنات التي تم اكتشافها.
ملاحظة هامة:القائمة rects تتضمن إحداثيات مربعات الإحاطة متمثلة بالزاويتين العلوية اليسارية والسفلية اليمنية ،بالتالي يتم حساب المركز انطلاقا من الزاويتين بالاعتماد على المعادلات التالية :
cX = (startX + endX) / 2
cY = (startY + endY ) /2
حيث :
(cX , cY ) :مركز مربع الإحاطة
(startX , startY) : الزاوية العلوية اليسارية
(endX , endY) :الزاوية السفلية اليمنية
الحلقة التكرارية من أجل حساب مركز كل كائن وإسناده إلى قائمة مراكز الكائنات الموجودة في الإطار الحالي.
الشيفرة البرمجية :
# initialize an array of input centroids for the current frame
inputCentroids = np.zeros((len(rects), 2), dtype="int")
# loop over the bounding box rectangles
for (i, (startX, startY, endX, endY)) in enumerate(rects):
cX = int((startX + endX) / 2.0)
cY = int((startY + endY) / 2.0)
inputCentroids[i] = (cX, cY)
تستخدم التعليمة الشرطية لإختبار عدم وجود أي كائن متتبع أو الكائنات المتتبعة سابقاً قد اختفت جميعها (أي قاموس الكائنات المتتبعة فارغ) ،في هذه الحالة أي تحقق الشرط يتم تنفيذ الحلقة التكرارية ليتم تسجيل جميع الكائنات المكتشفة بواسطة طريقة تسجيل كائن .
الشيفرة البرمجية :
# if we are currently not tracking any objects take the input
# centroids and register each of them
if len(self.objects) == 0:
for i in range(0, len(inputCentroids)):
self.register(inputCentroids[i])
أما في حال لم يتحقق الشرط السابق سيتم اختبار هل الكائنات المكتشفة هي مرتبطة بالكائنات السابقة أم لا ،وبالتالي نعرف قائمتين القائمة الأولى قائمة معرفات الكائنات المتتبعة objectIDs والتي يتم الحصول عليها من قيم المفاتيح key لقاموس الكائنات المتتبعة والقائمة الثانية هي مراكز الكائنات المتتبعة objectcentroids ويتم الحصول عليها من القيم values لقاموس الكائنات المتعقبة.
بعد ذلك يتم حساب المسافة الإقليدية بين مركز كل كائن من الكائنات المتتبعة (القديمة) مع كل كائن من الكائنات المكتشفة باستخدام تابع حساب المسافة cdist الموجود ضمن مكتبةُ الحَوسَبةِ العلميَّةِ .
سنتحدث قليلاً عن تابع حساب المسافة :
هو تابع جاهز موجود ضمن مكتبةُ الحَوسَبةِ العلميَّةِ يُستخدم من أجل حساب المسافة الإقليدية ،دخله عبارة عن مصفوفتين ثنائيتين بشرط ان تكون أعمدة المصفوفة الأولى مساوية لأعمدة المصفوفة الثانية .
توضيح : قلنا أن تابع حساب المسافة الذي يحسب المسافة بين النقاط يقبل مصفوفتين كدخل وبُعد كل مصفوفة يجب أن يكون ثنائي،وذلك لأن أسطر المصفوفة تمثل عدد النقاط والأعمدة تمثل إحداثيات النقاط.
فعلى سبيل المثال في حال كانت المصفوفة تحتوي 5 أسطر بالتالي يوجد خمس نقاط،أما بالنسبة للأعمدة في حال كان عدد أعمدة المصفوفة هو 2 بالتالي النقاط في المستوى الاحداثي أي (X,Y) أما في حال كان عدد الأعمدة هو 3 بالتالي النقاط في الفراغ أي (X,Y,Z) ،ولهذا السبب يجب ان تكون أعمدة المصفوفة الأولى مساوياً لأعمدة المصفوفة الثانية.
لدينا مصفوفة مراكز الكائنات المتتبعة في الإطار القديم و مصفوفة مراكز الكائنات الموجودة في الإطار الحالي .
بفرض قيم هذه المصفوفات بالشكل التالي :
مصفوفة مراكز الكائنات
0.95 | 0.37 |
0.95 | 0.73 |
مصفوفة مراكز الدخل
0.16 | 0.15 |
0.86 | 0.05 |
0.70 | 0.60 |
وبفرض أن (0.95 , 0.37) يعبر عن مركز الكائن المتتبع الأول الذي له معرف 0 و (0.95, 0.73 ) يعبر عن مركز الكائن المتتبع الثاني الذي له معرف 1 ،كلاهما في الإطار القديم.
وأيضاً (0.16 , 0.15 ) يعبر عن مركز الكائن الأول المكتشف و ( 0.86 ,0.05 ) يعبر عن مركز الكائن الثاني المكتشف و (0.70 , 0.60 ) يعبر عن مركز الكائن الثالث المكتشف ،جميعهم في الإطار الحالي ونريد ربط هذه الكائنات المكتشفة في الإطار الحالي مع الكائنات في الإطار القديم.
بعد تطبيق تابع حساب المسافة cdist تكون مصفوفة خرج هذا التابع D بالشكل :
0.33 | 0.32 | 0.82 |
0.16 | 0.727 | 0.726 |
إذا قُمنا بتحليل مصفوفة الخرج سوياً ،نلاحظ أن الكائن الأول المتتبع ذو المعرف 0 مرتبط بالكائن الثاني المكتشف ذو المركز ( 0.86 ,0.05 ) أي بمعنى من خلال موقع أصغر قيمة في السطر الأول من مصفوفة الخرج يمكن معرفة الكائن 0 بمن هو مرتبط من الكائنات المكتشفة .
ونلاحظ أن الكائن الثاني المتتبع ذو المعرف 1 مرتبط بالكائن الثالث المكتشف ذو المركز (0.70 , 0.60) كذلك من خلال موقع أصغر قيمة في السطر الثاني من مصفوفة الخرج يمكن معرفة الكائن ذو المعرف 1 بمن هو مرتبط .
حتى تتم العملية السابقة أي معرفة كل كائن في الإطار الحالي بمن مرتبط من الإطار السابق يتم القيام بما يلي :
أولاً – تعريف قائمة أسطر rows مهمتها إيجاد أصغر عنصر في كل سطر من أسطر مصفوفة خرج تابع المسافة بعد ذلك يتم تطبيق تابع الترتيب argsort عليها والذي يعمل على ترتيب أدلة عناصر القائمة rows حسب القيم الأصغر .
بالتالي القائمة rows الناتجة عن تطبيق أول مرحلة هي [0.32 , 0.16] بعدها يتم تطبيق تابع الترتيب عليها فتصبح [ rows = [1 , 0
وهذا يعني أن أصغر قيمة تقع في السطر الثاني من مصفوفة خرج تابع المسافة وثاني أصغر قيمة تقع في السطر الأول من المصفوفة D.
ثانياً – تعريف قائمة أعمدة cols ،هي تعتمد على قيم القائمة rows ,حيث قائمة الأعمدة الناتجة هي [2,1] .
القيمة 2 في القائمة cols ناتجة عن جواب السؤال التالي :
ماهو دليل العمود الذي يمتلك أصغر قيمة في السطر الثاني من مصفوفة خرج تابع المسافة ؟ هو العمود الثالث أي ( 2 ) .( قلنا سطر ثاني لأن أول قيمة في القائمة rows هي 1 أي سطر ثاني)
القيمة واحد في القائمة cols ناتجة عن جواب السؤال التالي :
ماهو دليل العمود الذي يمتلك أصغر قيمة في السطر الأول من مصفوفة خرج تابع المسافة ؟ هو العمود الثاني اي (1 ).( قلنا سطر أول لأن ثاني قيمة في القائمة rows هي صفر)
ثالثاً – القيام بعملية الدمج بين قائمتي ال rows و cols من خلال تابع الدمج zip .
توضيح بسيط حول عمل التابع zip :
تتوضح مهمة هذا التابع بتشكيل الثنائيات (a, b ) حيث a عنصر من القائمة rows و b عنصر من القائمة cols . وهكذا يتم المرور على جميع عناصر القائمتين rows و cols وتشكيل الثنائيات .
بالتالي تُصبح القائمة الناتجة بعد تطبيق تابع zip :
[(0,1) , (2, 1)]
أي الكائن المتتبع الثاني ذو المعرف 1 مرتبط بالكائن الثالث من الإطار الحالي.
والكائن المتتبع الأول ذو المعرف 0 مرتبط بالكائن الثاني من الإطار الحالي.
نلاحظ أن أسطر مصفوفة خرج تابع المسافة تدل على الكائنات في الإطار القديم أما بالنسبة للأسطر فهي تدل على الكائنات في الإطار الحالي.
الشيفرة البرمجية :
# otherwise, are are currently tracking objects so we need to
# try to match the input centroids to existing object
# centroids
else:
objectIDs = list(self.objects.keys())
objectCentroids = list(self.objects.values())
D = dist.cdist(np.array(objectCentroids), inputCentroids)
rows = D.min(axis=1).argsort()
cols = D.argmin(axis=1)[rows]
نعرف مجموعتين فارغتين المجموعة الاولى usedRows وهي ستحوي أدلة الكائنات الموجودة في الإطار القديم ولم تختفي في الإطار الحالي والمجموعة الثانية usedCols وهي ستحوي أدلة الكائنات المرتبطة في الإطار القديم ، الغاية منهما منع تخزين بيانات كائن أكثر من مرة .
الحلقة التكرارية من أجل المرور على جميع الكائنات المرتبطة بالإطار القديم من أجل تحديث مراكزها .
التعليمة الشرطية الأولى من أجل معرفة هل الكائن تم تخزين مركزه سابقاً ،إذا كان الشرط محقق يتم تجاهل باقي تعليمات الحلقة أي تجاهل خطوة تحديث المركز .
التعليمة الشرطية الثانية من أجل اختبار المسافة بين الكائن في الإطار القديم مع الكائن الموجود في الإطار الحالي بحيث إذا كانت المسافة بينهما أكبر من المسافة العظمى بالتالي الكائن في الإطار الحالي هو كائن جديد مختلف تماماً عن الكائن في الإطار القديم وهنا لدينا عملتين لابد من معالجتهما :
- بالنسبة للكائن في الإطار القديم هو في حالة اختفاء في الإطار الحالي بالتالي لابد من زيادة قيمة الاختفاء الخاصة بهذا الكائن في قاموس الكائنات المختفية .
- بالنسبة للكائن الجديد في الإطار الحالي يجب القيام بعملية التتبع له وذلك بإعطاءه معرف خاص به وإضافته أيضاً إلى القاموس الكائنات المتتبعة بواسطة الطريقة تسجيل كائن.
اذا لم تتحقق التعليمة الشرطية الأولى والثانية وبالتالي من المؤكد ان الكائن الموجود في الإطار القديم (الذي مركزه موجود في الموقع ذو الدليل r في القائمة مراكز الكائنات المتتبعة ) هو مرتبط بالكائن الموجود في الإطار الحالي ( الذي مركزه موجود في الموقع ذو الدليل c في القائمة مراكز الكائنات الموجودة في الإطار الحالي) بالتالي يجب القيام بما يلي :
- الكائن المرتبط بالكائن القديم يجب أن يأخذ نفس قيمة المعرف .
- تحديث مركز الكائن القديم في قاموس الكائنات المتتبعة بمركز الكائن الحالي ( أي مركز الكائن الموجود في قائمة مراكز الكائنات الموجودة في الإطار الحالي )
- تحديث قيمة الاختفاء الخاصة بالكائن بالقيمة صفر أي بمعنى (أن الكائن بعدما أن تغير موضعه هو في حالة ظهور أي تصبح عدد الإطارات المتتالية التي اختفي فيها هي صفر).
- إضافة دليل الكائن في الإطار القديم إلى مجموعة الكائنات الموجودة في الإطار القديم ولم تختفي في الإطار الحالي وأيضاً إضافة دليل الكائن في الإطار الحالي إلى مجموعة الكائنات المرتبطة في الإطار القديم.
الشيفرة البرمجية :
# in order to determine if we need to update, register,
# or deregister an object we need to keep track of which
# of the rows and column indexes we have already examined
usedRows = set()
usedCols = set()
# loop over the combination of the (r, c) index
# tuples
for (r, c) in zip(rows, cols):
if r in usedRows or c in usedCols:
continue
if D[r, c] > self.maxDistance:
continue
objectID = objectIDs[r]
self.objects[objectID] = inputCentroids[c]
self.disappeared[objectID] = 0
usedRows.add(r)
usedCols.add(c)
بعد إيجاد الكائنات المرتبطة بالإطار السابق , يجب معرفة مايلي :
أولاً – الكائنات التي كانت موجودة في الإطار السابق ولم تظهر في الإطار الحالي بالتالي هي في حالة اختفاء ( أي يجب زيادة قيمة الاختفاء الخاصة بها ) .
ثانياً – الكائنات المتتبعة في الإطار الحالي ( أي التي ليست مرتبطة مع الإطار القديم) بالتالي هي كائنات جديدة يجب القيام بعملية تتبع لها بواسطة طريق تسجيل كائن .
لمعرفة ذلك سوف نقوم بما يلي :
- تعريف المجموعة الأسطر غير المستخدمة unusedRows وهي مجموعة الكائنات التي لم تظهر في الإطار الحالي.
- تعريف المجموعة الأعمدة غير المستخدمة unusedCols وهي مجموعة الكائنات الجديدة في الإطار الحالي .
الشيفرة البرمجية :
# compute both the row and column index we have NOT yet
# examined
unusedRows = set(range(0, D.shape[0])).difference(usedRows)
unusedCols = set(range(0, D.shape[1])).difference(usedCols)
التعليمة الشرطية من أجل اختبار اذا كان عدد أسطر مصفوفة خرج تابع المسافة أكبر أو يساوي عدد أعمدتها أي بمعنى إذا كان عدد الكائنات المتتبعة في الإطار القديم أكبر أو يساوي عدد الكائنات الجديدة في الإطار الحالي بالتالي يوجد كائن واحد على الأقل لم يظهر في الإطار الحالي بالتالي هو في حالة اختفاء .
إذا تحقق الشرط عندها يتم المرور على أدلة مراكز الكائنات في مجموعة الكائنات التي لم تظهر في الإطار الحالي والتي تحوي أدلة الكائنات التي لم تظهر في الإطار التالي ونقوم بما يلي :
- يتم زيادة قيمة الاختفاء الخاصة بكل كائن دليله يوجد في مجموعة الكائنات التي لم تظهر في الإطار الحالي .
- في حال أصبحت قيمة اختفاء كائن أكبر من قيمة الاختفاء الأعظمي بالتالي يتم إلغاء تتبع هذا الكائن وحذفه بواسطة طريقة إلغاء تسجيل كائن.
الشيفرة البرمجية :
# in the event that the number of object centroids is
# equal or greater than the number of input centroids
# we need to check and see if some of these objects have
# potentially disappeared
if D.shape[0] >= D.shape[1]:
for r in unusedRows:
objectID = objectIDs[r]
self.disappeared[objectID] += 1
if self.disappeared[objectID] > self.maxDisappeared:
self.deregister(objectID)
وإلا اذا لم يتحقق الشرط السابق بالتالي عدد أسطر مصفوفة خرج تابع المسافة أصغر من عدد أعمدتها أي بمعنى عدد الكائنات المتتبعة في الإطار القديم أصغر من عدد الكائنات الموجودة في الإطار الحالي بالتالي يوجد كائن واحد على الأقل في الإطار الحالي هو غير مرتبط بالإطار القديم وبالتالي هو كائن جديد يجب إضافته إلى قاموس الكائنات المتتبعة ليتم تتبعه.
حيث يتم المرور على أدلة الكائنات الموجودة في مجموعة الكائنات الجديدة في الإطار الحالي وإضافة هذه الكائنات الى قاموس الكائنات المتتبعة بواسطة طريقة تسجيل كائن.
الشيفرة البرمجية :
# otherwise, if the number of input centroids is greater
# than the number of existing object centroids we need to
# register each new input centroid as a trackable object
else:
for column in unusedCols:
self.register(inputCentroids[column])
return self.objects
تطبيق عملي لتتبع الوجه باستخدام خوارزمية تتبع الكائنات
في تطبيقنا هذا سنقوم بتنفيذ خوارزمية تتبع الكائنات على تتبع الوجوه أي سنعتبر الوجه هو الكائن المراد تتبعه ,بالتالي نحتاج في البداية الى نموذج للكشف عن الوجه ,سنستخدم نموذج كافيه Caffe المدرب مسبقاً , لذلك تحتاج الى الملفين الخاصين بهذا النموذج وهما ملف معرف بنية النموذج وملف النموذج
res10_300x300_ssd_iter_140000.caffemodel
deploy.prototxt
الأن نقوم باستيراد المكتبات التالية :
from imutils.video import VideoStream
import numpy as np
import imutils
import time
import cv2
المكتبةُ المفتوحةُ للرُّؤيةِ الحاسُوبيَّةِ cv2 هي مكتبة مفتوحة المصدر تضم مجموعة من التوابع البرمجية التي تهدف بشكل رئيسي إلى التعامل مع تطبيقات الرؤية الحاسوبية في الزمن الحقيقي .
مكتبة imutils نستخدمها لتغير حجم الإطار.
مكتبة time من أجل إحداث تأخير زمني .
VideoStream من أجل الوصول الى الكاميرا .
الأن نقوم بتهيئة المتحولات التالية :
prototxt_path ='./deploy.prototxt'
model_path='./res10_300x300_ssd_iter_140000.caffemodel'
confidence=0.5
حيث prototxt_path هو مسار معرف بنية النموذج وهو ملف نصي يحتوي على معلومات حول بنية الشبكة العصبونية لنموذج كافيه وتتضمن هذه المعلومات :
- طبقات الشبكة العصبونية.
- البارامترات في كل طبقة وأبعاد الدخل والخرج .
- الاتصال بين الطبقات.
والمتحول model_path يمثل مسار نموذج كافيه Caffe المستخدم للكشف عن الوجه وهو نموذج مدرب مسبقاً pre-trained.
المتحول confidence يمثل عتبة احتمالية يتم ضبطها بشكل اختياري و بالاعتماد على هذه القيمة يتم التخلص من اكتشافات الوجوه الضعيفة أي الاكتشافات التي تقل عن هذه العتبة.
الأن نقوم بأخذ كائن من صف CentroidTracker ولهذا الصف كما ذكرنا سابقا ثلاث طرائق methods هي :
- تسجيل كائن register: لتسجيل كائن جديد والقيام بعملية التتبع له .
- إلغاء تسجيل كائن deregister : لإلغاء تتبع كائن.
- تحديث update : لتحديث مراكز الكائنات المتتبعة .
لكننا سنستخدم الطريقة الثالثة فقط تحديث لأنها ستقوم بتسجيل الكائنات وإلغاء التسجيل بشكل تلقائي .
نقوم بتهيئة H و W (أبعاد الإطار) بالقيمة الفارغة None .
تحميل نموذج كافيه كاشف الوجه
تشغيل متدفق الفيديو VideoStream وبدء عمل حساس الكاميرا , لكن نقوم بعمل تأخير زمني بمقدار 2 ثانية الى أن تتجهز الكاميرا .
الشيفرة البرمجية :
ct = CentroidTracker()
(H, W) = (None, None)
print("[INFO] loading model...")
net = cv2.dnn.readNetFromCaffe(prototxt_path, model_path)
print("[INFO] starting video stream...")
vs = VideoStream(src=0).start()
time.sleep(2.0)
الحلقة التكرارية while من أجل الوصول إلى إطارات الكاميرا والقيام بعملية المعالجة عليها بالزمن الحقيقي .
قراءة إطار من الكاميرا ثم القيام بتغير حجم هذا الإطار إلى عرض ثابت مع الحفاظ على نسبة العرض إلى الطول aspect ratio
التعليمة الشرطية if تنفذ إذا لم يسند طول وعرض الإطار إلى المتحولين H و W بعد .
نمرر الإطار الملتقط من الكاميرا الى التابع blobFromImage ,ومن خلال هذا التابع نستطيع تحديد عملية المعالجة المسبقة التي نريد تطبيقها على هذا الإطار قبل إدخاله إلى الشبكة العصبونية ,سنقوم بتوضيح المعاملين الثاني والرابع لهذا التابع اللذين يحددان المعالجة المسبقة :
بالنسبة للمعامل الثاني هو معامل التَّقييسُ scale factor وظيفة هذا المعامل تحجيم قيم بكسلات الإطار إلى نطاق معين , أما بالنسبة للمعامل الرابع فهو متوسط الطرح Mean Subtraction.
نبدأ بشرح معامل متوسط الطرح : يستخدم هذا المعامل للمساعدة في معالجة تغيرات الإضاءة في الصور المدخلة , لذلك يمكن اعتباره تقنية تستخدم لمساعدة شبكات الطي العصبونية ، ولإستخدام هذا المفهوم نقوم قبل البدء في تدريب الشبكة العصبونية بحساب متوسط كثافة البيكسل لكل طبقة (الحمراء والخضراء والزرقاء) لجميع الصور الموجودة في مجموعة البيانات dataset
هذا يعني أنه نحصل على ثلاثة قيم:
µR , µG , µB
عندما نمرر صورة عبر شبكة (سواء للتدريب أو الاختبار) ، فإننا نطرح المتوسط من كل قيم البيكسلات في الصورة المدخلة بالشكل التالي :
R = R – µR
G = G – µG
B = B – µB
مع تابع blobFromImage افترضنا أن متوسط قيم البيكسلات للطبقات الثلاثة هي (123.0 , 177.0 , 104.0) وذلك لأن نموذج كافيه هو نموذج مدرب مسبقاً على مجموعة بيانات تدريب ImageNet والقيم المتوسطة لهذه البيانات هي µR=104.0 و µG=177.0 و µB=123.0 بالتالي سوف نعتمد هذه القيم.
أما بالنسبة للمعامل Scale factor يتم استخدامه عندما نريد تطبيق عملية التَّقييسُ على قيم بيكسلات صورة لجعلها تنحصر ضمن مجال معين وفقا المعادلات التالية :
R = (R – µR) / σ
G = (G – µG) / σ
B = (B – µB) / σ
كذلك مع التابع blobFromImage افترضنا أن قيمة معامل التَّقييسُ هي 1.0 أي أننا لم نقم بعمل تقييسُ لقيم بكسلات الصورة وإنما اكتفينا بتطبيق متوسط الطرح فقط.
التابع setInput لتنفيذ المعالجة المسبقة على الإطار .
التابع forward للقيام بعملية تمرير للأمام للإطار عبر الشبكة العصبونية من أجل اكتشاف جميع الوجوه الموجودة في الإطار.
نقوم بتهيئة القائمة rects بقائمة فارغة والتي هي قائمة مربعات الإحاطة للوجوه المكتشفة في الإطار ،ليتم إسناد إليها مربعات الإحاطة للوجوه المكتشفة فيما بعد.
الشيفرة البرمجية :
while True:
frame = vs.read()
frame = imutils.resize(frame, width=400)
if W is None or H is None:
(H, W) = frame.shape[:2]
blob = cv2.dnn.blobFromImage(frame, 1.0, (W, H), (104.0, 177.0, 123.0))
net.setInput(blob)
detections = net.forward()
rects = []
نستخدم الحلقة التكرارية من أجل المرور على جميع تنبؤات النموذج كافيه المطبق على الإطار ، نستخدام التعليمة الشرطية if من أجل التخلص من التنبؤات الضعيفة التي تقل عن حد العتبة ،إن قيم إحداثيات مربع الإحاطة Bounding Box مطبق عليها تَّقييسُ أي مجال قيمها هو [0,1] وبالتالي يجب ضرب هذه القيم في طول وعرض الإطار للحصول على الإحداثيات الصحيحة لمربع الإحاطة على الإطار .
نقوم بإضافة أبعاد مربع الإحاطة للوجه المكتشف الى القائمة rects.
إن تابع القصر astype يستخدم لقصر القيم الى نوع محدد من البيانات , نقوم هنا بقصر قيم إحداثيات مربع الإحاطة الى نوع البيانات الصحيحة Integer وذلك من أجل التخلص من الفواصل العشرية.
نقوم برسم مربع الاحاطة حول الوجه المكتشف وذلك باستخدام تابع رسم المستطيل rectangle من المكتبةُ المفتوحةُ للرُّؤيةِ الحاسُوبيَّةِ,حيث هذا التابع يقبل الوسطاء التالية :
- الوسيط الأول : الإطار المراد رسم مربع الإحاطة عليه.
- الوسيط الثاني والثالث : إحداثيات مربع الإحاطة ممثلة بالزاويتين العليا اليسارية والسفلى اليمنية .
- الوسيط الرابع : لون الخط المستخدم ,حيث المكتبةُ المفتوحةُ للرُّؤيةِ الحاسُوبيَّةِ تتعامل مع صور ملونة بترتيب الألوان BGR فعلى سبيل المثال نمرر القيمة (255,0,0) بالنسبة للّون الأزرق.
- الوسيط الأخير : سماكة الخط .
الشيفرة البرمجية :
# loop over the detections
for i in range(0, detections.shape[2]):
if detections[0, 0, i, 2] > confidence:
box = detections[0, 0, i, 3:7] * np.array([W, H, W, H])
rects.append(box.astype("int"))
(startX, startY, endX, endY) = box.astype("int")
cv2.rectangle(frame, (startX, startY), (endX, endY), (0, 255, 0), 2)
تحديث متعقب الكائنات باستخدام طريقة التحديث وذلك بتمرير قائمة مربعات الإحاطة للوجوه المكتشفة في الإطار الحالي.
الحلقة التكرارية for للمرور على جميع الكائنات التي يجري تتبعها (أي الوجوه ) ،حيث نقوم بكتابة كل من الرقم المعرف ID والنقطة المركزية centroid لكل كائن متتبع في إطار الخرج وذلك باستخدام تابع كتابة النص putText وتابع رسم الدائرة circle من مكتبة OpenCV .
أولاً – بالنسبة لتابع كتابة النص يستخدم هذا التابع لكتابة نص على الإطار و هنا نستخدم هذا التابع من أجل كتابة معرف للكائن ، يقبل هذا التابع الوسطاء التالية :
- الوسيط الأول : الإطار المراد الكتابة عليه .
- الوسيط الثاني : السلسلة النصية المراد كتابتها على الإطار.
- الوسيط الثالث :موضع النص بالنسبة للإطار (إحداثيات الزاوية السفلية اليسارية للنص).
- الوسيط الرابع : نوع الخط المستخدم.
- الوسيط الخامس : عامل مقياس الخط الذي يتم ضربه في حجم الخط الأساسي.
- الوسيط السادس : لون الخط .
- الوسيط السابع : سماكة الخط .
ثانياً – بالنسبة لتابع رسم الدائرة يستخدم لرسم دائرة على الإطار حيث يقبل هذا التابع الوسطاء التالية :
- الوسيط الأول : الإطار المراد رسم الدائرة عليه.
- الوسيط الثاني : إحداثيات مركز الدائرة .
- الوسيط الثالث :نصف قطر الدائرة .
- الوسيط الرابع : لون الخط المستخدم .
- الوسيط الخامس :سماكة الخط المستخدم في رسم حدود الدائرة ،لكن في حال تم استخدام القيمة 1- هذا يعني أنه سيتم ملئ الدائرة باللون المحدد ( أي تلوين الدائرة من الداخل وعدم الاكتفاء برسم حدود الدائرة فقط ).
التابع waitKey وهو تابع الربط مع لوحة المفاتيح حيث يتمكن هذا التابع من التنصت على لوحة المفاتيح و جلب الأحداث منها ومعالجتها (المقصود هنا بالأحداث أي عند الضغط على مفتاح معين على لوحة المفاتيح )
- يقبل هذا التابع وسيط هو التأخير الزمني delay يقدر بالميلي ثانية حيث يمثل الوقت الزمني الذي يستغرقه هذا التابع في التنصت على لوحة المفاتيح وجلب الحدث منها ، فعند تمرير قيمة 10 مثلا الى هذا التابع بالتالي سينتظر مدة زمنية قدرها 10 ميلي ثانية ويجلب الحدث الحاصل على لوحة المفاتيح خلال هذه المدة ،لكن في حال تم تمرير القيمة 0 بالتالي سيتم الانتظار إلى أجل غير مسمى.
- يعيد هذا التابع رمز المفتاح المضغوط عليه أو يعيد القيمة 1- في حال لم يتم الضغط على أي مفتاح قبل انتهاء وقت التنصت.
التابع waiyKey يعيد قيمة عددية 32 بت حيث البتات الثمانية أقصى اليسار تمثل القيمة العددية لرمز المفتاح المضغوط عليه بترميز ASCII لذلك نحن نهتم فقط بهذه البتات الثمانية ونريد أن تكون جميع البتات الاخرى صفر بالتالي للقيام بذلك نقوم بتطبيق عملية AND بين القيمة المعادة لتابع waitKey و العدد 0xFF .
العدد 0xFF هو بالنظام الست عشري Hexadecimal وبالتالي العدد المقابل له في النظام الثنائي هو 11111111 أي 8 بت فقط لذلك يتم استخدامه للحصول على أخر 8 بت .
القيمة المعادة من ( ord( char تمثل قيمة الترميز ASCII المقابلة للمحرف المرر char وبالتالي بالمقارنة مع هذا العدد يمكننا التحقق من وجود مفتاح مضغوط وكسر الحلقة.
ملاحظة : التابع waitKey يعمل فقط في حال إنشاء نافذة HighGUI واحدة على الأقل وكانت نشطة .
وبالتالي التعليمة الشرطية if في حال تم الضغط على مفتاح q على لوحة المفاتيح يتم الخروج من الحلقة while وإنهاء التنفيذ.
التابع imshow يستخدم لإظهار إطار الخرج الناتج بعد إجراء عملية المعالجة عليه.
التابع destroyAllWindows لإغلاق جميع النوافذ التي تم إنشاؤها (حيث يتم إنشاء نافذة لعرض إطار الخرج الناتج عند استخدام التابع imshow).
الشيفرة البرمجية :
# update our centroid tracker using the computed set of bounding
# box rectangles
objects = ct.update(rects)
for (objectID, centroid) in objects.items():
text = "ID {}".format(objectID)
cv2.putText(frame, text, (centroid[0] - 10, centroid[1] - 10), cv2.FONT_HERSHEY_SIMPLEX,
0.4, (0, 0, 255), 1)
cv2.circle(frame, (centroid[0], centroid[1]), 4, (0, 255, 0), -1)
cv2.imshow("Frame", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
break
cv2.destroyAllWindows()
vs.stop()
يمكنكم الاطلاع على كامل الشيفرة البرمجية في مستودع الجيت هب الخاص بموقع الذكاء الاصطناعي باللغة العربية
فيديو يظهر خرج التطبيق العملي
الخاتمة
في هذا المقال تعرّفنا على خوارزمية تتبع الكائنات والتي هي واحدة من أهم الخوارزميات المستخدمة في مجال الرؤية الحاسوبية، تحدَثنا عن آلية عمل هذه الخوارزمية بشكل تفصيلي و استعرضنا أهم المعاملات التي تتعامل معها هذه الخوارزمية ثم بعد ذلك قمنا بتحقيق هذه الخوارزمية ، طبقنا مثال عملي على تتبع الوجوه ،أخيراً رأينا سويَاً فيديو يوضح نتيجة خرج التطبيق العملي .
المصادر
1-https://www.pyimagesearch.com/2018/07/23/simple-object-tracking-with-opencv/.
2-https://medium.com/visionwizard/object-tracking-675d7a33e687.
تعليق واحد
ممكن الربط مع الاردوينو لتنفيذ العملية