اكتشاف قناع الوجه باستخدام OpenCV و Keras و TensorFlow

كتبت بواسطة Mohammad Kardi | بتاريخ الأربعاء 20 ايار 2020

مقدمة :

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


سنناقش طريقين لاكتشاف الشخص أن كان يرتدي قناع الوجه ام لا و سنتعلم كيف يمكن تحقيق ذلك باستخدام الـDeep Learning  .


خطوات انشاء النموذج بشكل عام :

  1.  بناء نموذج مهمته تصنيف هل الشخص يضع قناع ام لا بواسطة Python وتدريبه على dataset  باستخدام مكتبة TensorFlow وKeras  .
  2.  التأكد من كفاءة النموذج من خلال حساب ال Accuracy  .



النموذج السابق يمكن تحقيقه بطريقتين :

  1. اكتشاف  الأقنعة الموجودة في الصور .
  2. اكتشاف الأقنعة الموجودة في الفيديوهات .


خطوات انشاء النموذج بشكل خاص :


سنقسم المشروع الى قسمين مستقلين عن بعضهمها البعض بحيث كل قسم يمتلك خطواته الخاصة به :

  1. Training  :هي عبارة عن مرحلة تدريب النموذج بحيث يتم فيها اولاً تحميل الـ dataset  الخاصة يتصنف قناع الوجه (الوجه بقناع / بدون قناع) , ثم تدريب النموذج على هذه الداتا عندها نكون قد انتهينا من تصميم مصنف قادر على معرفة هل الشخص قد يضع قناع ام لا , بعد ذلك نقوم بحفظ الموديل المدرب في الذاكرة .
  2. Deployment  : في هذه المرحلة سنقوم باجراء تطبيق لهذا النموذج على صور اشخاص , حيث في البدابة نقوم باكتشاف الوجه ثم تصنيف الوجه اذا كان بقناع أو لا .



الان سنتعرف على dataset المستخدمة في تدريب الموديل.



هي عبارة عن 1376 صورة تنقسم الى قسمين : 

  1. 690 صورة لوجوه تحوي قناع .
  2. 686 صورة لوجوه لاتحوي قناع.


وهدفنا من هذه الـ dataset كما ذكرنا سابقاً , تدريب نموذج تعلم عميق مخصص لإكتشاف الأشخاص الذي يرتدون قناع أو لا .


هيكل المشروع :



المجلد dataset يحوي على مجموعة البيانات التي سنستخدمها لتدريب واختبار النموذج ,يتألف هذا المجلد من مجلدين ,الأول مجلد يدعى  with_mask يضم بداخله 690 صورة لوجوه تحوي قناع , المجلد الثاني يدعى without_mask يضم بداخله 686 صورة لوجوه لاتحوي قناع .

المجلد  examples يحوي على ثلاثة أمثلة (الصور) لاستخدامها في اختبار نموذج كشف قناع الوجه face mask detector .

المجلد face_detector : يحوي على نموذج لكتشاف الوجه وهو نموذج مدرب بشكل مسبقpre-trained.

لدينا أيضا ثلاث ملفات Python  :

  1. الملف train_mask_detector : يحوي هذا الملف على النموذج المستخدم لتصنيف قناع الوجه(الوجه بقناع / بدون قناع).
  2. الملف detect_mask_image : يقوم هذا الملف باكتشاف قناع الوجه في الصور ثابتة.
  3. الملف detect_mask_video : باستخدام الكاميرا الخاصة بك يقوم هذا الملف باكتشاف قناع الوجه على كل إطار متدفق من الكاميرا.


في البداية سنقوم بتثبيت الحزم (المكتبات) التي سنتعامل معها :

pip install tensorflow
pip install sklearn
pip install numpy
pip install matplotlib
pip install opencv-python
pip install imutils
pip install argparse
pip install time


ملاحظة : حزمة os  تأتي بشكل تلقائي مع ملف بايثون لا داعي لتثبيتها .

ملاحظة : في حال هذه المكتبات موجودة ضمن بيئة العمل الخاصة بك لا داعي لتثبيتها .

سنقوم بأخذ مثال عن تثبيت أحد المكتبات على سبيل المثال مكتبة Scikit-Learn

 نقوم بفتح الـ terminal  ونضع التعليمة بالشكل التالي :

C:\Users\ASUS>pip install sklearn


 تحقيق COVID-19 face mask detector باستخدام Keras  و TensorFlow  :

(ملف train_mask_detector.py)

اولاً :

# import the necessary packages
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras.layers import AveragePooling2D
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Input
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
from tensorflow.keras.preprocessing.image import img_to_array
from tensorflow.keras.preprocessing.image import load_img
from tensorflow.keras.utils import to_categorical
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from imutils import paths
import matplotlib.pyplot as plt
import numpy as np
import argparse
import os

استيراد مكتبة TensorFlow  و Keras  لكي نقوم بما يلي :

  1. زيادة البيانات Data Augmentation .
  2. تحميل ال MobilNetV2 Classifier هو عبارة عن نموذج CNN  تم تدريبه مسبقاً سنستخدمه في بناء النموذج الحالي .
  3. بناء شبكة Fully Connected  .
  4. المعالجة المسبقة للبيانات Pre-Processing .


استيراد مكتبة Scikit-Learn  لكي نقوم بطباعة تقرير عن أداء الموديل .

استيراد مكتبة matplotlib  حتى نقوم برسم منحني التدريب بالاعتماد على تابع الخطأ .

ثانيا :

نعرف وسطاء سطر الأوامر command line arguments  المطلوبة لتشغيل برنامجنا

قبل معرفة ما هي الوسطاء المطلوب تعريفها سنأخذ لمحة توضيحية بسيطة نوضح فيها مفهوم  command line arguments

التعامل مع وسطاء سطر الأوامر هي مهارة أولية يجب أن تتعلم كيفية استخدامها ، خاصة إذا كنت تحاول تطبيق مفاهيم التعلم العميق أو معالجة الصور أو رؤية الحاسوبية المتقدمة computer vision,حيث هذه الوسطاء تعطي معلومات إضافية الى برنامجنا خلال زمن التشغيل runtime  وهذا ما يسمح لنا أن نعطي برنامجنا مدخلات مختلفة على الفور دون تغيير الكود.

نقوم بتعريف الوسيط التالي (سنخرج عن مشروعنا قليلاً لتوضيح الفكرة السابقة) :

بفرض لدينا تعليمات التالية :

# import the necessary packages
import argparse

# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-n", "--name", required=True,
	help="name of the user")
args = vars(ap.parse_args())

# display a friendly message to the user
print("Hi there {}, it's nice to meet you!".format(args["name"]))

نقوم في البداية باستيراد المكتبة argparse التي تمكننا من تعريف وسطاء سطر الأوامر.

نقوم بإنشاء كائن ArgumentParser ونسميه ap .

نضيف argument  حيث يمكن تحديد تسمية الوسيط باستخدام تسمية مختصرة –n أو تسمية طويلة --name   , يمكن استخدام أي منهما في command line.

بالنسبة لـ required=True أي يجب اسناد قيمة للوسيط من سطر الأوامر.

السلسلة help تعطي معلومات مساعدة في terminal .

نطبق  vars على الكائن لتحويل وسيط سطر الأوامر إلى قاموس Python  (تذكرة : قاموس Python  يحوي على مفتاح key وقيمة Value) حيث يكون المفتاح هنا هو اسم وسيط سطر الأوامر (سواء تسمية مختصرة أو طويلة) والقيمة هي القيمة المقدم لوسيط سطر الأوامر (سواء تم ادخال هذه القيمة من سطر الأوامر أو اعطيناه قيمة افتراضية سنذكر كيفية إعطاء قيمة افتراضية لوسيط لاحقا).

لإدخال قيمة الى الوسيط الذي عرفناه سابقا name نقوم بفتح الـ terminal  والذهاب الى مسار الموجود فيه ملف python  واسناد قيم الى المتحول كما نلاحظ في الشكل التالي حيث اعطينا البرنامج مدخلات مختلفة دون تغيير الكود في زمن التشغيل .

$ python simple_example.py --name Adrian
Hi there Adrian, it's nice to meet you!
$
$ python simple_example.py --name Stephanie
Hi there Stephanie, it's nice to meet you!
$
$ python simple_example.py --name YourNameHere
Hi there YourNameHere, it's nice to meet you!


بالنسبة لإعطاء قيمة افتراضي للوسيط حيث يتم اسناد هذه القيمة له في حال عدم ادخال القيمة من سطر الأوامر بالشكل التالي :

ap.add_argument("-n", "--name", type=str, default="Mohammad",
	help="name of the user")


حيث type  هي نوع الوسيط String , و default هي القيمة الافتراضية.

بالعودة الى مشروعنا اصبح من السهل علينا فهم الوسطاء التالية :

# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", required=True,
	help="path to input dataset")
ap.add_argument("-p", "--plot", type=str, default="plot.png",
	help="path to output loss/accuracy plot")
ap.add_argument("-m", "--model", type=str,
	default="mask_detector.model",
	help="path to output face mask detector model")
args = vars(ap.parse_args())

حيث :

dataset-- : مسار الملف الذي يحوي dataset.

plot-- : مسار حفظ الصورة المولدة باستخدام مكتبة matplotlib ,توضح هذه الصورة منحنى التدريب ومنحنى الدقة validation خلال عملية التدريب.

model-- : مسار الذي سوف نحفظ فيه النموذج بعد الانتهاء من تدريبه .


ثالثا :

نعرف معاملات الموديل من معدل  التعليم  learning rate و epochs و batch size.

# initialize the initial learning rate, number of epochs to 
# train for, and batch size
INIT_LR = 1e-4
EPOCHS = 20
BS = 32

سوف نقوم بتطبيق learning rate decay  ولهذا السبب سمينا معدل التعلم INIT_LR .

رابعا : 

# grab the list of images in our dataset directory, then 
# initialize the list of data (i.e., images) and class images

#Block 1
print("[INFO] loading images...")
imagePaths = list(paths.list_images(args["dataset"]))
data = []
labels = []

# loop over the image paths
for imagePath in imagePaths:
    #Block 2
	# extract the class label from the filename
	label = imagePath.split(os.path.sep)[-2]
     
    #Block 3
	# load the input image (224x224) and preprocess it
	image = load_img(imagePath, target_size=(224, 224))
	image = img_to_array(image)
	image = preprocess_input(image)

	# update the data and labels lists, respectively
	data.append(image)
	labels.append(label)

#Block 4
# convert the data and labels to NumPy arrays
data = np.array(data, dtype="float32")
labels = np.array(labels)

نقوم بقراءة جميع مسارات الصور الموجودة في ملف dataset  , حيث أن المتحول imagePaths  هو عبارة عن قائمة list  يحوي مسارات الصور(مسارات الصور وليس صور) Block 1.

حلقة for  من أجل كل مسار موجود في القائمة imagePaths

في البداية نقوم بقراءة اسم المجلد التي توضع فيه الصورة التي مسارها imagePath ونضعها في متحول label لمعرفة تسمية هذه الصورة (كما ذكرنا سابقا أن ملف dataset يحوي مجلدين هما with_mask و without_mask) الـ Block 2.

نقوم بتحميل الصورة باستخدام التابع load_img حيث نمرر له مسار الصورة والبعد المطلوب , نجعل هنا البعد 224×224 وذلك لأن النموذج  الذي سنستخدمها يتعامل مع صور ملونة بأبعاد 224×224 بيكسل , Block 3 .

استخدام التابع img_to_array لتحويل الصورة من نمط بايثون للصور (Python Imaging Library (PIL إلى مصفوفة بايثون العددية .

استخدام التابع preprocess_input وذلك لتحويل مجال قيم بكسلات صورة من 0-255 إلى 0-1 وهذا يفيد في تسريع عملية التدريب .

نقوم بإضافة صورة و تسميتها المقابلة كلاً منهما في list .

تحويل كلاً من المصفوفتين data و labels  من مصفوفة بايثون العددية إلى مصفوفة  NumPy , الـ  Block 4.


خامساً :

#Block 1
# perform one-hot encoding on the labels
lb = LabelBinarizer()
labels = lb.fit_transform(labels)
labels = to_categorical(labels)

#Block 2
# partition the data into training and testing splits using 80% 
# of the data for training and the remaining 20% for testing
(trainX, testX, trainY, testY) = train_test_split(data, labels,
	test_size=0.20, stratify=labels, random_state=42)

#Block 3
# construct the training image generator for data augmentation
aug = ImageDataGenerator(
	rotation_range=20,
	zoom_range=0.15,
	width_shift_range=0.2,
	height_shift_range=0.2,
	shear_range=0.15,
	horizontal_flip=True,
	fill_mode="nearest")


LabelBinarizer يقوم بتعيين رقم فريد لكل تسمية مختلفة فعلى سبيل المثال يقوم بتعين القيمة 0 لفئة الخرج  without_mask والقيمة 1 لفئة الخرج with_mask.

يقوم التابع to_categorical بترميز القيم الفئوية كمصفوفة one hot encoding

وبالتالي تحويل كل قيمة في الخرج الى شعاع يحوي على القيمة 1 وما تبقى من القيم هي صفرية, Block 1.

تقسم البيانات الى 80% كبيانات تدريب و 20 % كبيانات اختبار باستخدام مكتبة Sklearn, الـ Block 2.

ثم توليد بيانات صور جديدة اعتماداً على بيانات الصور القديمة من خلال عمل مؤثرات على الصور مثل تقريب, تدوير ,ازاحة , انعكاس افقي او عمودي وهذا الخطوة تسمى بالـ Data Augmentation  وتساهم هذه الخطوة في عملية الـ generalization  للنموذح مما يجعله اكثر كفاءة , Block 3 .


سادساً:

#Block 1
# load the MobileNetV2 network, ensuring the head FC layer sets 
# are left off
baseModel = MobileNetV2(weights="imagenet", include_top=False,
	input_tensor=Input(shape=(224, 224, 3)))

#Block 2
# construct the head of the model that will be placed on top of 
# the base model
headModel = baseModel.output
headModel = AveragePooling2D(pool_size=(7, 7))(headModel)
headModel = Flatten(name="flatten")(headModel)
headModel = Dense(128, activation="relu")(headModel)
headModel = Dropout(0.5)(headModel)
headModel = Dense(2, activation="softmax")(headModel)

#Block 3
# place the head FC model on top of the base model (this will be
# come the actual model we will train)
model = Model(inputs=baseModel.input, outputs=headModel)

#Block 4
# loop over all layers in the base model and freeze them so they 
# will *not* be updated during the first training process
for layer in baseModel.layers:
	layer.trainable = False


تحميل النموذج MobileNet وهو عبارة عن شبكة CNN  تم تدريبها مسبقاً على مجموعة البيانات ImageNet مع الاخد بعين الاعتبار يتم تجاهل طبقة الخرج include_top=False , الـ Block 1.

بناء طبقة خرج جديدة توافق الصور الموجودة في dataset الخاصة بالنموذج COVID-19 face mask detector  , الـ Block 2.

قمنا بإزالة طبقة الخرج لنموذج MobileNet لأن هذا النموذج مدرب بشكل مسبق على الف نوع من صور (صور قط , كلاب , إشارات مرور . . .الخ ) لذلك يقوم بتصنيف الصورة المدخلة  إليه الى صنف واحد من بين الف صنف (أي طبقة الخرج تحوي الف عصبون ) وهذا مالا يتوفق مع نموذجنا لأننا نريد في هذا النموذج تصنيف الصورة المدخلة الى صنفين رئيسين أما with_mask أو without_mask أي جعل طبقة الخرج تحوي عصبونين رئيسين

وهذا ما يعرف بنمط "Transfer Learning"

وتعد هذه طريقة شائعة في مجال Deep Learning نظراً للموارد الهائلة والوقت الكثير اللازم لتطوير نماذج الشبكات العصبية المطلوبة لمثل هذه المهام 

ثلاث فوائد لاستخدام  "Transfer Learning "

  1. توفير الوقت اللازم في بناء النموذج (حيث كما رأينا سابقا أننا قمنا ببناء طبقة خرج وطبقة خفية قبلها فقط).
  2. لا نحتاج الى كمية كبيرة من البيانات.
  3. توفير الوقت اللازم لتدريب النموذج حيث يبدأ النموذج في اول epoch  أثناء التدريب بدقة عالية.


قمنا بدمج طبقة الخرج التي تم بناؤها مع شبكة الـ Mobile Net  التي تم تحميلها فينتج لدينا شبكة جديدة طبقة الخرج فيها هي طبقة تم بناؤها اما طبقة الدخل هي نفسها .بذلك قد استفدنا من قيم الأوزان المثالية لطبقة الدخل , Block 3.

عملية تدريب الشبكة ككل  تعتمد ع الـ backpropagation  اي تعديل الأوزان بناء على على تابع الخطأ cost Function  ولدينا اوزان طبقة دخل تم ضبطها مسبقاً  لذلك نقوم بتجميد هذه الاوزان وإيقاف تعديلها اثناء عملية  Backpropagation , الـ Block 4 .


سابعاً :

#Block 1
# compile our model
print("[INFO] compiling model...")
opt = Adam(lr=INIT_LR, decay=INIT_LR / EPOCHS)
model.compile(loss="binary_crossentropy", optimizer=opt,
	metrics=["accuracy"])

#Block 2
# train the head of the network
print("[INFO] training head...")
H = model.fit(
	aug.flow(trainX, trainY, batch_size=BS),
	steps_per_epoch=len(trainX) // BS,
	validation_data=(testX, testY),
	validation_steps=len(testX) // BS,
	epochs=EPOCHS)


تعريف خصائص الموديل :

تابع الخطأ حيث استخدمنا هنا تابع الخطأ binary crossentropy لان لدينا صنفين من البيانات فقط , والمحسن  الذي استخدمناه هو محسن آدم Adam Optimizer ,وطريقة قياس اداء الموديل هي accuracy , الـ Block 1 .

تدريب الموديل وهنا عملية التدريب فقط تتضمن تعديل أوزان طبقة الخرج , Block 2.

استخدمنا هنا اثناء عملية التدريب مفهوم Validation  وهو مفهوم مفيد لتحقق من كفاءة النموذج أثناء عملية التدريب ومنعه من الوقوع في مشكلة overfitting  , ولابد من التنويه الى أمر مهم جداً أن مجموعة البيانات validation dataset فقط مستخدمة لتحقق من كفاءة النموذج على الرغم من استخدامها خلال مرحلة التدريب الا أنه لا يتم استخدامها ابدا لعملية تعديل الاوزان.

ملاحظة : في نموذجنا هذا جعنا بيانات Validation  هي نفسها بيانات الاختبار (أي test dataset )نظراً لقلة البيانات لدينا , لكن في حال توفر بيانات كثيرة لابد من  تقسيم البيانات الى ثلاثة اقسام القسم الاول للتدريب training dataset والثاني للتحقق من كفاءة التدريب validation dataset اما الثالث هو جزء منفصل تماما عن عملية التدريبtest dataset  وهذا الجزء يستخدم لمرحلة اختبار كفاءة النموذج النهائية قبل تعميمه (أي اختبار النموذج على بيانات لم يراها من قبل ).


ثامناً :

#Block 1
# make predictions on the testing set
print("[INFO] evaluating network...")
predIdxs = model.predict(testX, batch_size=BS)

# for each image in the testing set we need to find the index of 
# the label with corresponding largest predicted probability
predIdxs = np.argmax(predIdxs, axis=1)

# show a nicely formatted classification report
print(classification_report(testY.argmax(axis=1), predIdxs,
	target_names=lb.classes_))

#Block 2
# serialize the model to disk
print("[INFO] saving mask detector model...")
model.save(args["model"], save_format="h5")


بعد اكتمال عملية التدريب سنقوم بتقييم النموذج الناتج على بيانات الإختبار Test dataset  , الـ Block 1.

 القيام بعملية حفظ الموديل , Block 2.


تاسعاً :

# plot the training loss and accuracy
N = EPOCHS
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, N), H.history["loss"], label="train_loss")
plt.plot(np.arange(0, N), H.history["val_loss"], label="val_loss")
plt.plot(np.arange(0, N), H.history["accuracy"], label="train_acc")
plt.plot(np.arange(0, N), H.history["val_accuracy"], label="val_acc")
plt.title("Training Loss and Accuracy")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend(loc="lower left")
plt.savefig(args["plot"])


بالنهاية نقوم برسم منحنى التدريب ومنحنى الدقة ومن خلالهما يمكن معرفة حالة النموذج اذا كان يعاني من مشاكل معينة ام لا .


بعد ان انتهينا من عملية بناء الموديل سنقوم بعملية التدريب لإختبار كفاءته .

حيث نقوم بفتح  terminal وننفذ الامر التالي :

$ python train_mask_detector.py --dataset dataset



كما نلاحظ ان معدل الـ Accuracy  لـ validation set  تقريبا 99 %   وتعتبر نسبة عالية , ويوجد اشارات بسيطة تدل على ظهور الـ overfitting لأن لدينا قيمة تابع الخطأ لبيانات لـ Validation  هي اقل بشيء بسيط من قيمة تابع الخطأ لبيلتات لـ Training . من خلال هذه النتائج نستنتج ان الموديل هو قابل للتعميم من اجل اي دخل .



تحقيق   COVID-19 face mask detector  للصور  باستخدام OpenCV  :

(ملف detect_mask_image.py)

اولاً : 

# import the necessary packages
from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
from tensorflow.keras.preprocessing.image import img_to_array
from tensorflow.keras.models import load_model
import numpy as np
import argparse
import cv2
import os

نستدعي المكتبات السابقة.


ثانياً :

# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,
	help="path to input image")
ap.add_argument("-f", "--face", type=str,
	default="face_detector",
	help="path to face detector model directory")
ap.add_argument("-m", "--model", type=str,
	default="mask_detector.model",
	help="path to trained face mask detector model")
ap.add_argument("-c", "--confidence", type=float, default=0.5,
	help="minimum probability to filter weak detections")
args = vars(ap.parse_args())


image -- : مسار الصورة المدخلة المطلوب تصنيفها هل تحوي قناع ام لا .

face --:مسار النموذج المستخدم لاكتشاف الوجوه في الصورة .

model --: مسار النموذج الذي قُمنا ببنائه لتصنيف قناع الوجه (الوجه بقناع / بدون قناع).  .

confidence-- : عتبة احتمالية اختيارية threshold لتخلص من اكتشافات الوجوه الضعيفة.


ثالثاً :

#Block 1
# load our serialized face detector model from disk
print("[INFO] loading face detector model...")
prototxtPath = os.path.sep.join([args["face"], "deploy.prototxt"])
weightsPath = os.path.sep.join([args["face"],
	"res10_300x300_ssd_iter_140000.caffemodel"])
net = cv2.dnn.readNet(prototxtPath, weightsPath)

#Block 2
# load the face mask detector model from disk
print("[INFO] loading face mask detector model...")
model = load_model(args["model"])


تحميل نموذج اكتشاف الوجوه من OpenCV وهو نموذج pre-trained , الـ Block 1 .

حيث يعتمد على إطار (SSD (Single Shot MultiBox Detector ، باستخدام نموذج ResNet-10.

ملاحظة هامة : نموذج اكتشاف الوجه يختلف عن نموذج تصنيف قناع الوجه (الوجه بقناع / بدون قناع) الذي دربناه سابقا ,أي بشكل مختصر آلية العمل كالتالي في البداية نقوم باكتشاف جميع الأوجه الموجودة في صورة معينة (قد تحوي الصورة اكثر من وجه) باستخدام نموذج اكتشاف الوجه ,ثم نقوم بعمل قص لصورة الوجه الموجودة في الصورة الاصلية ونمرر هذه الصورة ( الوجه المقصوص) الى نموذج تصنيف قناع الوجه لمعرفة اذا كان الوجه يحوي قناع أم لا ثم نقوم بعد ذلك برسم مستطيل حول الوجه ,وهذا سيتم توضيحه برمجيا.

بالنسبة لملف prototxt  يعرف بنية النموذج أي ترتيب الخلايا العصيبة في كل طبقة, وانماط الاتصال بين الطبقات ,ووظائف التنشيط , وكيفية تحويل الشبكة لمدخلاتها الى مخرجات .

الملفcaffemodel  يحوي على اوزان الطبقات الفعلية.

تحميل نموذج اكتشاف قناع الوجه , Block 2.


رابعاً :

#Block 1
# load the input image from disk, clone it, and grab the image spatial
# dimensions
image = cv2.imread(args["image"])
orig = image.copy()
(h, w) = image.shape[:2]

#Block 2
# construct a blob from the image
blob = cv2.dnn.blobFromImage(image, 1.0, (300, 300),
	(104.0, 177.0, 123.0))

#Block 3
# pass the blob through the network and obtain the face detections
print("[INFO] computing face detections...")
net.setInput(blob)
detections = net.forward()


عند القيام بتحميل الصورة من الذاكرة يتم اخد نسخة منها من اجل الاستفادة منها لاحقاً , Block 1 .

التابع blobFromImage يقوم بتغيير حجم الصورة  Image لتصبح 300*300  لكي تتوافق هذه الصورة مع نموذج اكتشاف الوجه caffemodel وذلك لأن هذا النموذج يقبل صورة دخل بأبعاد 300×300 بيكسل

بالنسبة للقيم (104.0, 177.0, 123.0)  هي عبارة عن Mean subtraction

سنتحدث قليلاً عن هذا المفهوم :

 متوسط ​​الطرح Mean subtraction يستخدم هذا المفهوم للمساعدة في معالجة تغيرات الإضاءة في الصور المدخلة , لذلك يمكن اعتبرها تقنية تستخدم لمساعدة الشبكات العصبونية التلافيفية.

حيث قبل البدء في تدريب الشبكة العصبونية ، نقوم أولاً بحساب متوسط ​​كثافة البكسل لكل طبقة ( الحمراء والخضراء والزرقاء) لجميع الصور الموجودة في  dataset.

هذا يعني أننا ننتهي بثلاثة متغيرات:

µR , µG , µB


عندما نمرير صورة عبر شبكة (سواء للتدريب أو الاختبار) ، فإننا نطرح المتوسط ​​من كل قيم البكسلات في الصورة المدخلة بالشكل التالي :

R = R - µR
G = G - µG
B = B - µB


ملاحظة هامة : القيم المتوسطة لمجموعة بيانات تدريب ImageNet هي R = 103.93 و G = 116.77 و B = 123.68 , وبالتالي سوف تستخدم هذه القيم إذا كنت تستخدم شبكة تم تدريبها مسبقًا على بيانات ImageNet , وهذا سبب وضعنا للقيم (104.0, 177.0, 123.0) في التابع blobFromImage.


وقد يكون لدينا عامل scale  (سيجما ) والذي يضاف في Normalization

R = (R - µR) / σ

G = (G - µG) / σ

B = (B - µB) / σ


يستخدم هذا العامل لتحجيم قيم بكسلات الصورة المدخلة إلى نطاق معين

ستجد في اغلب الحالات يتم تنفيذ عملية الطرح فقط (وبالتالي إعداد sigma = 1.0)

الوسيط الثاني لمتحول blobFromImage هو scale factor حيث مررنا القيمة 1.0 أي قمنا بتنفيذ عملية الطرح فقط .

ملاحظة هامة : يقوم تابع blobFromImage بشكل تلقائي يتحويل الصورة الملونة من الترتيب RGB الى ترتيب  BGR وذلك لان مكتبة OpenCV  تتعامل مع صور الملونة بالترتيب BGR


 التابع setInput لتطبيق تنسيق blob على الصورة المدخلة .

التابع forward يقوم بعملية التنبؤ بجميع الوجوه في الصورة للقيام بتصنيفها فينا بعد , Block.

حيث خرج النموذج (أي detections) هو مصفوفة 4D حيث :

البعد الثالث يمثل الوجوه المكتشفة .

البعد الرابع يعطي معلومات عن احداثيات الـ bounding box  والاحتمالية confidence على سبيل المثال [detections[0,0,0,2 تعطي قيمة confidence  للوجه الأول و [detections[0,0,0,3:6 يعطي احداثيات bounding box للوجه الاول

مثال اخر [:,detections[0,0,2 يعطي معلومات عن الوجه الثالث المكتشف

ملاحظة : الـ condidence هي احتمالية أن يطابق bounding box  الوجه .


خامساً :

# loop over the detections
for i in range(0, detections.shape[2]):
	# extract the confidence (i.e., probability) associated with the detection
	confidence = detections[0, 0, i, 2]

	# filter out weak detections by ensuring the confidence is
	# greater than the minimum confidence
	if confidence > args["confidence"]:
        #Block 1
  		# compute the (x, y)-coordinates of the bounding box for the object
  		box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
  		(startX, startY, endX, endY) = box.astype("int")
  
        #Block 2
  		# ensure the bounding boxes fall within the dimensions of
  		# the frame
  		(startX, startY) = (max(0, startX), max(0, startY))
  		(endX, endY) = (min(w - 1, endX), min(h - 1, endY))


حلقة for  تمر على جميع الاحتماليات لتوقع وجود الوجوه في الصورة

تعليمة if  لتصفية الاحتماليات الضعيفة التي تقل عن حد العتبة threshold

خرج احداثيات bounding box مطبق عليها Normalization أي مجال قيمها [0،1], وبالتالي يجب ضرب هذه الاحداثيات في طول وعرض الصورة الأصلية للحصول على bounding box الصحيح على الصورة ,Block 1 .


التحقق اذا كان ال bounding box  لكل وجه هو داخل حدود الصورة , Block 2 .


سادساً :

        # extract the face ROI, convert it from BGR to RGB channel
		# ordering, resize it to 224x224, and preprocess it
		face = image[startY:endY, startX:endX]
		face = cv2.cvtColor(face, cv2.COLOR_BGR2RGB)
		face = cv2.resize(face, (224, 224))
		face = img_to_array(face)
		face = preprocess_input(face)
		face = np.expand_dims(face, axis=0)

        #Block 1
		# pass the face through the model to determine if the face
		# has a mask or not
		(mask, withoutMask) = model.predict(face)[0]


استخراج  ROI  من الصورة  باستخدام مكتبة numpy  وفي مثالنا ال ROI هو عبارة عن صورة الوجه   .

التابع cvtColor يقوم بتحويل صورة الوجه من BGR الى RGB بالتبديل بين قناتي R و B في الصورة.

التابع preprocess_input لقيام بعملية Pre-process لـ  ROI .

التابع expand_sims يستخدم هذا التابع لتوسيع بعد المصفوفة حيث أن صورة الوجه face  هي ثلاث أبعاد

ونموذج تصنيف قناع الوجه (الوجه بقناع / بدون قناع) يتوقع الدخل لصورة ملونة رباعية الابعاد  224×224 بيكسل , لذلك نقوم بإضافة بعد رابع لصورة الوجهface .

القيام بعملية التنبؤ لمعرفة هل الوجه يحوي قناع ام لا , Block 1 .


سابعاً  :

        #Block 1        
        # determine the class label and color we'll use to draw
		# the bounding box and text
		label = "Mask" if mask > withoutMask else "No Mask"
		color = (0, 255, 0) if label == "Mask" else (0, 0, 255)

        #Block 2
		# include the probability in the label
		label = "{}: {:.2f}%".format(label, max(mask, withoutMask) * 100)

        #Block 3
		# display the label and bounding box rectangle on the output frame
		cv2.putText(image, label, (startX, startY - 10),
			cv2.FONT_HERSHEY_SIMPLEX, 0.45, color, 2)
		cv2.rectangle(image, (startX, startY), (endX, endY), color, 2)

#Block 4
# show the output image
cv2.imshow("Output", image)
cv2.waitKey(0)


تحديد اذا كان الوجه يحوي قناع او لا بناءً على القيمة المتوقعة من النموذج الـ mask detector ,

واذا كان الوجه يحوي قناع يتم اختبار اللون الأخضر , اما اذا كان الوجه لايحوي قناع يتم اختيار اللون الأحمر , Block 1.

حساب نسبة احتمال توقع الموديل , Block 2 .

وضع نص على الصورة يعبر عن احتمال توقع النموذج ,بالإضافة الى وضع اطار على شكل  مستطيل حول الوجه يتم ذلك باستخدام مكتبة openCv , الـ Block 3 .

لرسم مستطيل باستخدام مكتبة الـ OpenCV يجب تحديد زاوية المستطيل العليا اليسرى وزاويته السفلى اليمنى

البارومتر الأول لتابع رسم المستطيل هو لصورة أما بالنسبة لبارامتر الثاني والثالث هما إحداثيات زاوية المستطيل العليا اليسرى والسفلى اليمنى

البارامتر الثالث هو لون خط المستطيل بالنسبة لـ BRG  فأننا نمرر صف عل سبيل المثال :

نمرر الصف (255,0,0) بالنسبة للون الأزرق

ملاحظ : كما قلنا سابقا بأن مكتبة OpenCV  تتعامل مع صور الملونة بأخذ الألوان بالترتيب BGR.

البارامتر الرابع هو لسماكت خط المستطيل

حيث يقوم هذا التابع بتعديل الصورة واضافة المستطيل لها

لأضافه نص للصورة علينا تحديد :

النص الذي نريد إدراجه , موضع النص بالنسبة للصورة (الزاوية السفلى اليسرى ) ,نوع الخط ,عامل مقياس الخط (الذي يتم ضربه في الحجم الأساسي للخط), اللون , السماكة.


اظهار الصورة , Block 4 .


عملية تجريب COVID-19 face mask detector باستخدام OpenCV  على صور مختلفة:


1) تجربة النموذج على الصورة الأولى الموجودة ضمن مجلد examples :


نقوم بتحميل الصورة الأولى من خلال المسار الخاص بها

$ python detect_mask_image.py --image examples/example_01.png 


النتيجة :


نلاحظ ان الموديل face mask detector  قد نجح في عملية التنبؤ ( الشخص يضع قناع ) .


2)  تجربة النموذج على الصورة الثانية :

تحميل الصورة الثانية  من خلال المسار الخاص بها .

$ python detect_mask_image.py --image examples/example_02.png 


النتيجة :

نلاحظ ان النموذج face mask detector  قد نجح في عملية التنبؤ ( الشخص لايضع قناع ).


3) تجربة النموذج على الصورة الثالثة :

تحميل الصورة الثالثة من خلال المسار الخاص بها .

$ python detect_mask_image.py --image examples/example_03.png 


النتيجة :


هنا قد حدثت مشكلة في النموذج نلاحظ أن الشخصين الذين يقفان في الخلف قد نجح النموذج بتنبأ انهما لا يرتديان أقنعة ولكن الفتاة التي تقف في الأمام تم تجاهلها من قبل النموذج وسبب ذلك ان النموذج ينقسم الى قسمين :

1)  مرحلة اكتشاف الوجه .

2)  مرحلة تصنيف هل الوجه يحوي كمامة ام لا .

المشكلة التي حصلت ان النموذج لم يستطع التعرف على الوجه اولا ً كما نلاحظ ان حجم الكمامة كبير جداً وتكاد تغطي معظم معالم الوجه .

ولحل هذه المشكلة يجب ان يتم تدريب النموذج على صور وجوه يوجد عليها أقنعة باحجام كبيرة .


نتيجة تنفيذ الموديل على صورة لي :


نلاحظ ان النموذج  قد نجح في عملية التنبؤ (لا أضع قناع ).




تنفيذ تطبيق كاشف قناع الوجه في الوقت الفعلي عبر فيديو مباشر باستخدام مكتبة OpenCV :

(ملف detect_mask_video.py)

نحن نعلم كيف يمكن تطبيق كاشف قناع الوجه على صورة ثابتة ولكن ماذا عن تطبيقه عبر فيديو مباشر ؟

الخوارزمية التي نستخدمها لتطبيق كاشف قناع الوجه عبر فيديو (أو كاميرا) مباشر هي الخوارزمية ذاتها المستخدمة في تطبيق كاشف قناع الوجه في الصور ولكن مع اضافة Methods  تسمح لنا بمعالجة كل frame  من الفيديو بشكل مستقل وذلك عن طريق استدعاء الصف VideoStream  والصف time .


حيث في البدية نقوم باستيراد المكتبات التالية :

# import the necessary packages
from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
from tensorflow.keras.preprocessing.image import img_to_array
from tensorflow.keras.models import load_model
from imutils.video import VideoStream
import numpy as np
import argparse
import imutils
import time
import cv2
import os


يقوم التابع التالي باكتشاف الوجوه الموجودة في الصورة أولا , ثم تطبق مصنف قناع الوجه على كلROL .

def detect_and_predict_mask(frame, faceNet, maskNet):
	# grab the dimensions of the frame and then construct a blob
	# from it
	(h, w) = frame.shape[:2]
	blob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300),
		(104.0, 177.0, 123.0))

	# pass the blob through the network and obtain the face detections
	faceNet.setInput(blob)
	detections = faceNet.forward()

	# initialize our list of faces, their corresponding locations,
	# and the list of predictions from our face mask network
	faces = []
	locs = []
	preds = []
	# loop over the detections
	for i in range(0, detections.shape[2]):
		# extract the confidence (i.e., probability) associated with
		# the detection
		confidence = detections[0, 0, i, 2]

		# filter out weak detections by ensuring the confidence is
		# greater than the minimum confidence
		if confidence > args["confidence"]:
			# compute the (x, y)-coordinates of the bounding box for
			# the object
			box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
			(startX, startY, endX, endY) = box.astype("int")

			# ensure the bounding boxes fall within the dimensions of
			# the frame
			(startX, startY) = (max(0, startX), max(0, startY))
			(endX, endY) = (min(w - 1, endX), min(h - 1, endY))
			# extract the face ROI, convert it from BGR to RGB channel
			# ordering, resize it to 224x224, and preprocess it
			face = frame[startY:endY, startX:endX]
			face = cv2.cvtColor(face, cv2.COLOR_BGR2RGB)
			face = cv2.resize(face, (224, 224))
			face = img_to_array(face)
			face = preprocess_input(face)
			face = np.expand_dims(face, axis=0)

			# add the face and bounding boxes to their respective
			# lists
			faces.append(face)
			locs.append((startX, startY, endX, endY))
	# only make a predictions if at least one face was detected
	if len(faces) > 0:
		# for faster inference we'll make batch predictions on *all*
		# faces at the same time rather than one-by-one predictions
		# in the above `for` loop
		preds = maskNet.predict(faces)

	# return a 2-tuple of the face locations and their corresponding
	# locations
	return (locs, preds)


هذا التابع يأخذ ثلاث وسطاء :

frame : الاطار المتدفق من الكاميرا .

faceNet : النموذج المستخدم لاكتشاف الوجوه من صورة .

maskNet : نموذج تصنيف قناع الوجه .

حيث يعيد هذا التابع قائمتين Two lists القائمة الأولى تمثل احداثيات الـ bounding box للوجوه المكتشفة يقابلها تصنيف كل وجه (قناع / دون قناع ) في القائمة preds.

كما ذكرنا سابقا أن الخوارزمية في هذا الملف هي نفسها في الملف السابق ، ولكن يتم تجميعها بطريقة تسمح بمعالجة كل إطار متدفق من الكاميرا ,وبالتالي لن نعيد شرح هذه التعليمات فهي مشروحة سابقا.


# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-f", "--face", type=str,
	default="face_detector",
	help="path to face detector model directory")
ap.add_argument("-m", "--model", type=str,
	default="mask_detector.model",
	help="path to trained face mask detector model")
ap.add_argument("-c", "--confidence", type=float, default=0.5,
	help="minimum probability to filter weak detections")
args = vars(ap.parse_args())


وسطاء سطر الأوامر هي :

face -- : مسار نموذج اكتشاف الوجه.

model --: مسار نموذج تصنيف قناع الوجه (قناع/بدون قناع).

confidence-- : عتبة احتمالية threshold.



نقوم باستيراد للنماذج السابقة :

# load our serialized face detector model from disk
print("[INFO] loading face detector model...")
prototxtPath = os.path.sep.join([args["face"], "deploy.prototxt"])
weightsPath = os.path.sep.join([args["face"],
	"res10_300x300_ssd_iter_140000.caffemodel"])

faceNet = cv2.dnn.readNet(prototxtPath, weightsPath)

# load the face mask detector model from disk
print("[INFO] loading face mask detector model...")
maskNet = load_model(args["model"])

# initialize the video stream and allow the camera sensor to warm up
print("[INFO] starting video stream...")
vs = VideoStream(src=0).start()
time.sleep(2.0)



لدينا الحلقة التالية الخاصة بالإطارات المتدفقة من الكاميرا :

# loop over the frames from the video stream
while True:
	# grab the frame from the threaded video stream and resize it
	# to have a maximum width of 400 pixels
	frame = vs.read()
	frame = imutils.resize(frame, width=400)

	# detect faces in the frame and determine if they are wearing a
	# face mask or not
	(locs, preds) = detect_and_predict_mask(frame, faceNet, maskNet)
	# loop over the detected face locations and their corresponding
	# locations
	for (box, pred) in zip(locs, preds):
		# unpack the bounding box and predictions
		(startX, startY, endX, endY) = box
		(mask, withoutMask) = pred

		# determine the class label and color we'll use to draw
		# the bounding box and text
		label = "Mask" if mask > withoutMask else "No Mask"
		color = (0, 255, 0) if label == "Mask" else (0, 0, 255)

		# include the probability in the label
		label = "{}: {:.2f}%".format(label, max(mask, withoutMask) * 100)

		# display the label and bounding box rectangle on the output
		# frame
		cv2.putText(frame, label, (startX, startY - 10),
			cv2.FONT_HERSHEY_SIMPLEX, 0.45, color, 2)
		cv2.rectangle(frame, (startX, startY), (endX, endY), color, 2)

	# show the output frame
	cv2.imshow("Frame", frame)
	key = cv2.waitKey(1) & 0xFF

	# if the `q` key was pressed, break from the loop
	if key == ord("q"):
		break

# do a bit of cleanup
cv2.destroyAllWindows()
vs.stop()


التابع ()vs.read لالتقاط اطار من الكاميرا.

سنستفيد أيضًا من مكتبة  imutils لتغيير حجم الاطار.

نطبق التابع detect_and_predict_mask على الاطار الملتقط لاكتشاف الوجوه وتصنيفها.

داخل حلقة for  نقوم بتطبيق نتائج التنبؤ من اجل كل وجه مكتشف حيث نقوم برسم مستطيل ووضع نص  حول كل وجه موجود في الاطار .


يتم التقاط ضغطات المفاتيح, إذا ضغط المستخدم على مفتاح (quit) ، فإننا نخرج من الحلقة.


الان نذهب الى terminal وننفذ الامر التالي :

$ python detect_mask_video.py






ملخص :

لأنشاء كاشف قناع الوجه قمنا بتدريب النموذج على صنفين من البيانات الصنف الاول لاشخاص يرتدون قناع الوجه والصنف الثاني لاشخاص لا يرتدون قناع الوجه , قمنا بضبط النموذج MobileNetV2 على هذه البيانات وحصلنا على دقة 99% تقريباً .

حيث أن عملية اكتشاف قناع الوجه مرت بمرحلتين اساسيتين :

1)  مرحلة اكتشاف الوجه .

2)  مرحلة تصنيف الوجه هل يحوي كمامة ام لا .

كما أن هذا النموذج يعاني من مشكلة اكتشاف الوجه في حال كانت الكمامة كبير جداً وتكاد تغطي معظم معالم الوجه ,وحل هذه المشكلة بتدريب الموديل على صور وجوه يوجد عليها أقنعة باحجام كبيرة .



بقلم :

محمد أحمد كردي

محمد مصطفى ذويب



رابط تحميل المشروع : http://shamra.sy/uploads/face-mask-detector.zip


المصادر :

https://towardsdatascience.com/finally-learn-how-to-use-command-line-apps-by-making-one-bd5cf21a15cd
https://towardsdatascience.com/a-comprehensive-hands-on-guide-to-transfer-learning-with-real-world-applications-in-deep-learning-212bf3b2f27a
https://medium.com/@vinuvish/face-detection-with-opencv-and-deep-learning-90bff9028fa8
http://datahacker.rs/face-detection-opencv-images/
https://www.pyimagesearch.com/2020/05/04/covid-19-face-mask-detector-with-opencv-keras-tensorflow-and-deep-learning/