Firebase MLKit التعرف على الوجوه
شرحنا سابقاً درس التعرف على النصوص باستخدام Firebase MLKit وسنستمر اليوم في MLKit وبالأخص التعرف على الوجوه (Face Detection)
ما وظيفة Face Detection ؟
بكل بساطة وظيفته أن يتعرف على ملامح الوجه , وفي حالة ML Kit فإنه يمكنه التعرف على الملامح التالية:
- الفم
- الوجنتين
- الأذنين
- الأنف
- العينين
int | BOTTOM_MOUTH | The center of the subject's bottom lip. |
int | LEFT_CHEEK | The midpoint between the subject's left mouth corner and the outer corner of the subject's left eye. |
int | LEFT_EAR | The midpoint of the subject's left ear tip and left ear lobe. |
int | LEFT_EYE | The center of the subject's left eye cavity. |
int | LEFT_MOUTH | The subject's left mouth corner where the lips meet. |
int | NOSE_BASE | The midpoint between the subject's nostrils where the nose meets the face. |
int | RIGHT_CHEEK | The midpoint between the subject's right mouth corner and the outer corner of the subject's right eye. |
int | RIGHT_EAR | The midpoint of the subject's right ear tip and right ear lobe. |
int | RIGHT_EYE | The center of the subject's right eye cavity. |
int | RIGHT_MOUTH | The subject's right mouth corner where the lips meet. |
بالإضافة الى التعرف هل العين مفتوحة, هل الشخص مبتسم أم لا وأيضاً تدعم الوضع Real-Time ,فإذا قمت بفتح الكاميرا فإنها تستطيع التعرف على ملامح الوجه دون الإنتظار لالتقاط الصورة.
ملاحظة:هناك فرق بين Face Detection و Face Recognition , ففي حالة Face Detection لا يمكنه إخبارك هل هذا الشخص هو "أحمد" او شخص آخر
سنبدأ بإنشاء مشروع على Firebase Console ونقوم بربطه ب Android Studio ثم نقوم بإضافة مكتبة Face Detection
implementation 'com.google.firebase:firebase-ml-vision:17.0.0'
لبدء التعرف على الوجه نحتاج الى أن نعرف FirebaseVisionImage كما فعلنا في الدرس السابق, ويمكننا الحصول عليها عن طريق:
- Bitmap
- ByteArray
- ByteBuffer
- FilePath (صورة jpg على سبيل المثال)
نقوم بتعريف FirebaseVisionImage بهذا الشكل, وفي حالتي سأستخدم خيار Bitmap حيث أنني سأقوم بأخذ الصورة عبر Intent
private fun pickImageFromGallery() {
val photoPickerIntent = Intent(Intent.ACTION_PICK)
photoPickerIntent.type = "image/*"
startActivityForResult(photoPickerIntent, PICK_IMG_REQUEST)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == PICK_IMG_REQUEST && resultCode == RESULT_OK) {
val uri = data?.data
val pickedImageBitmap = MediaStore.Images.Media.getBitmap(this.contentResolver, uri)
val visionImage = FirebaseVisionImage.fromBitmap(pickedImageBitmap)
}
}
ثم سنقوم بتعريف FirebaseVisionFaceDetectorOptions وهو سيخبر Firebase MLKit ما الأشياء التي نريدها, هل نريد التعرف على الفم فقط مثلاً , او نريد مثلاً ان نعرف هل الشخص مبتسم أم لا
في حالتنا سنقوم بالتعرف على جميع الملامح ,ولا يهمنا هل كان الشخص مبتسماً او لا. كما أنه قمنا بتشغيل الوضع الدقيق ACCURATE_MODE
val options = FirebaseVisionFaceDetectorOptions.Builder()
.setModeType(FirebaseVisionFaceDetectorOptions.ACCURATE_MODE)
.setLandmarkType(FirebaseVisionFaceDetectorOptions.ALL_LANDMARKS)
.build()
ثم نقوم بتعريف detector الذي سيبدأ بعملية التعرف , وقمنا بإعطائه options التي قمنا بإنشائها
val detector = FirebaseVision.getInstance()
.getVisionFaceDetector(options)
أخيراً نقوم ببدء التعرف على الوجه عبر الميثود detectInImage وأعطيناه visionImage وأضفنا onSuccessListener لنعرف اذا نجحت العملية
وonSuccessListener تعيد List<FirebaseVisionFace> تحتوي على كافة الوجوه التي تم التعرف عليها ,فإذا كان هنالك أكثر من وجه في الصورة فسيقوم بإكتشافه
detector.detectInImage(visionImage).addOnSuccessListener {
}
ولأخذ عنصر معين من الوجه نستخدم الميثود getLandmark ونعطيها اسم العنصر(العين,الأنف الخ..)
it.forEach {
val rightEye = it.getLandmark(FirebaseVisionFaceLandmark.RIGHT_EYE)
val leftEye = it.getLandmark(FirebaseVisionFaceLandmark.LEFT_EYE)
val leftMouth = it.getLandmark(FirebaseVisionFaceLandmark.LEFT_MOUTH)
val rightMouth = it.getLandmark(FirebaseVisionFaceLandmark.RIGHT_MOUTH)
val bottomMouth = it.getLandmark(FirebaseVisionFaceLandmark.BOTTOM_MOUTH)
val leftCheek = it.getLandmark(FirebaseVisionFaceLandmark.LEFT_CHEEK)
val rightCheek = it.getLandmark(FirebaseVisionFaceLandmark.RIGHT_CHEEK)
val leftEar = it.getLandmark(FirebaseVisionFaceLandmark.LEFT_EAR)
val rightEar = it.getLandmark(FirebaseVisionFaceLandmark.RIGHT_EAR)
val noseBase = it.getLandmark(FirebaseVisionFaceLandmark.NOSE_BASE)
}
ولتوضيح الفكرة بشكل أكبر سنقوم باختيار صورة ما ورسم دائرة على كل عنصر من عناصر الوجه داخل forLoop
ولهذا سنقوم بإنشاء Bitmap جديدة وهي عبارة عن نسخة من الصورة الأصلية ,ونقوم بإنشاء Canvas الذي سيقوم بالرسم على هذه الصورة
val newBitmap = pickedImageBitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(newBitmap)
الآن سنقوم بإنشاء ميثود نسميها drawLandmark ونعطيها Canvas و landmark
fun drawLandmark(canvas: Canvas, landmark: FirebaseVisionFaceLandmark?) {
if (landmark != null) {
canvas.drawCircle(landmark.position.x, landmark.position.y, 15f, getPaint())
}
}
ثم نقوم باستخدام الميثود drawCircle ونعطيها x عنصر الوجه و y وحجم الدائرة 15f وقمنا بإنشاء Paint وهو شكل خط الدائرة (هل هو Stroke ام Fill) بالإضافة الى اللون الخ..
private fun getPaint(): Paint {
val p = Paint()
p.setStyle(Paint.Style.STROKE)
p.setAntiAlias(true)
p.setFilterBitmap(true)
p.setDither(true)
p.setColor(Color.RED)
return p
}
الآن سنستدعي الميثود drawLandmark داخل forLoop لكل عنصر
ثم سنقوم بوضع Bitmap الجديد داخل ImageView لنرى الصورة
detector.detectInImage(visionImage).addOnSuccessListener {
val newBitmap = pickedImageBitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(newBitmap)
it.forEach {
val rightEye = it.getLandmark(FirebaseVisionFaceLandmark.RIGHT_EYE)
val leftEye = it.getLandmark(FirebaseVisionFaceLandmark.LEFT_EYE)
val leftMouth = it.getLandmark(FirebaseVisionFaceLandmark.LEFT_MOUTH)
val rightMouth = it.getLandmark(FirebaseVisionFaceLandmark.RIGHT_MOUTH)
val bottomMouth = it.getLandmark(FirebaseVisionFaceLandmark.BOTTOM_MOUTH)
val leftCheek = it.getLandmark(FirebaseVisionFaceLandmark.LEFT_CHEEK)
val rightCheek = it.getLandmark(FirebaseVisionFaceLandmark.RIGHT_CHEEK)
val leftEar = it.getLandmark(FirebaseVisionFaceLandmark.LEFT_EAR)
val rightEar = it.getLandmark(FirebaseVisionFaceLandmark.RIGHT_EAR)
val noseBase = it.getLandmark(FirebaseVisionFaceLandmark.NOSE_BASE)
drawLandmark(canvas, rightEye)
drawLandmark(canvas, leftEye)
drawLandmark(canvas, leftMouth)
drawLandmark(canvas, rightMouth)
drawLandmark(canvas, bottomMouth)
drawLandmark(canvas, leftCheek)
drawLandmark(canvas, rightCheek)
drawLandmark(canvas, leftEar)
drawLandmark(canvas, rightEar)
drawLandmark(canvas, noseBase)
}
image_view.setImageBitmap(newBitmap)
}
نقوم بتشغيل التطبيق واختيار صورة لنجد النتيجة
قد يقول أحدهم ماذا سأستفيد من هذا الأمر؟ بكل بساطة يمكنك إنشاء بعض الفلاتر مثل فلاتر سناب شات أو غيرها.
ولهذا سأقوم بإنشاء مثال بسيط على هذا الأمر, سنقوم بوضع عيون وفم كرتونية على صورة ما.
سنقوم أولاً برسم العيون ,ولهذا قمت بتحميل صورة كرتونية جاهزة فيها عين بشكل كرتوني وصورة أخرى للفم ووضعتهم داخل مجلد Drawable ثم نقوم بتحويلهم الى Bitmap
val eyeBitmap = BitmapFactory.decodeResource(resources, R.drawable.cartoon_eye)
val mouthBitmap = BitmapFactory.decodeResource(resources, R.drawable.mouth)
نقوم أولاً بأخذ حجم العين عبر أخذ عرض الوجه كاملاً وضربه ب 0.15f
val eyeSize = (it.boundingBox.width()) * 0.15f
نقوم بإنشاء ميثود نسميها drawEye
private fun drawEye(eyeBitmap: Bitmap, canvas: Canvas, eye: FirebaseVisionFaceLandmark, eyeSize: Float) {
canvas.save()
canvas.translate(eye.position.x, eye.position.y)
var rectF = RectF(-eyeSize, -eyeSize,
eyeSize, eyeSize)
canvas.drawBitmap(eyeBitmap, null, rectF, null)
canvas.restore()
}
قمنا في هذه الميثود باستخدام الميثود translate التي تقوم بتحريك مؤشر الرسم الى المكان الذي نريده, ولكن عند استخدام هذه الميثود يجب علينا استخدام save لبدء تحريك المؤشر واستخدام restore عند الانتهاء .
ثم قمنا بتعريف RectF وهو مربع يحتوي على الإحداثيات (يسار,فوق,يمين,أسفل) أخيراً قمنا برسم هذه bitmap بهذه الإحداثيات.
نقوم باستدعاء هذه الميثود للعين اليمنى واليسرى
detector.detectInImage(visionImage).addOnSuccessListener {
val newBitmap = pickedImageBitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(newBitmap)
val eyeBitmap = BitmapFactory.decodeResource(resources, R.drawable.cartoon_eye)
it.forEach {
val rightEye = it.getLandmark(FirebaseVisionFaceLandmark.RIGHT_EYE)
val leftEye = it.getLandmark(FirebaseVisionFaceLandmark.LEFT_EYE)
val eyeSize = (it.boundingBox.width()) * 0.15f
if (leftEye != null) {
drawEye(eyeBitmap, canvas, leftEye, eyeSize)
}
if (rightEye != null) {
drawEye(eyeBitmap, canvas, rightEye, eyeSize)
}
}
image_view.setImageBitmap(newBitmap)
}
نقوم بتشغيل التطبيق
بنفس الطريقة نقوم برسم الفم بعد إنشاء الميثود drawMouth
ولكن هنا قمنا بأخذ الجهة اليسرى واليمنى والسفلى من الفم بالإضافة الى أسفل الأنف لنستطيع حساب مكان الفم بشكل دقيق
private fun drawMouth(canvas: Canvas, mouthBitmap: Bitmap, face: FirebaseVisionFace) {
val leftMouth = face.getLandmark(FirebaseVisionFaceLandmark.LEFT_MOUTH)
val rightMouth = face.getLandmark(FirebaseVisionFaceLandmark.RIGHT_MOUTH)
val bottomMouth = face.getLandmark(FirebaseVisionFaceLandmark.BOTTOM_MOUTH)
val bottomNose = face.getLandmark(FirebaseVisionFaceLandmark.NOSE_BASE)
if (leftMouth != null && rightMouth != null && bottomMouth != null && bottomNose != null) {
val mouthSizeWidth = leftMouth.position.x - rightMouth.position.x
val mouthSizeHeight = bottomMouth.position.y - bottomNose.position.y
val centreX = mouthSizeWidth / 2 + rightMouth.position.x
val centreY = mouthSizeHeight / 2 + bottomNose.position.y + 20
canvas.save()
canvas.translate(centreX, centreY)
val rectF = RectF(-mouthSizeWidth / 2f * 1.5f, -mouthSizeHeight / 2f * 1.5f,
mouthSizeWidth / 2f * 1.5f, mouthSizeHeight / 2f * 1.5f)
canvas.drawBitmap(mouthBitmap, null, rectF, null)
canvas.restore()
}
}
ثم نستدعي هذه الميثود
detector.detectInImage(visionImage).addOnSuccessListener {
val newBitmap = pickedImageBitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(newBitmap)
it.forEach {
val rightEye = it.getLandmark(FirebaseVisionFaceLandmark.RIGHT_EYE)
val leftEye = it.getLandmark(FirebaseVisionFaceLandmark.LEFT_EYE)
val eyeSize = (it.boundingBox.width()) * 0.15f
if (leftEye != null) {
drawEye(eyeBitmap, canvas, leftEye, eyeSize)
}
if (rightEye != null) {
drawEye(eyeBitmap, canvas, rightEye, eyeSize)
}
drawMouth(canvas, mouthBitmap, it)
}
image_view.setImageBitmap(newBitmap)
}
نقوم بتشغيل التطبيق لنرى النتيجة :D
الآن اذا أردت وضع الفلاتر بالوضع الآني (Real Time) فأنصحك بالذهاب الى هذا Sample من Firebase الذي قام بتنفذ كافة الأمور المتعلقة بالكاميرا.
شخصياً قمت بتجربة الموضوع وخرجت بهذا المثال, وهو التحقق اذا كان الشخص مبتسم فقم بوضع Emoji سعيد والا قم بوضع Emoji حزين
ولهذا يجب عليك تشغيل وضع Tracking أولاً وثانياً وضع نوع Classification الى ALL لتعطينا MLKit المعلومات الإضافية (العين مفتوحة او الشخص مبتسم)..
FirebaseVisionFaceDetectorOptions options =
new FirebaseVisionFaceDetectorOptions.Builder()
.setClassificationType(FirebaseVisionFaceDetectorOptions.ALL_CLASSIFICATIONS)
.setLandmarkType(FirebaseVisionFaceDetectorOptions.ALL_LANDMARKS)
.setTrackingEnabled(true)
.build();
بعد ذلك قمت بالذهاب الى كلاس FaceGraphic.java الموجود داخل Sample
وقمت بأخذ المربع الذي يحتوي كامل الوجه , ثم قمت بالتحقق اذا كانت القيمة أكبر من 0.175f فعندها الشخص مبتسم ,والا فإن الشخص غير مبستم. يمكنك تجربة تغيير القيم لتحصل على أدق نسبة.
ثم قمنا ببعض الحسابات ورسمنا emoji على حسب حالة الشخص.
happyEmoji = BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.happy_emoji);
sadEmoji = BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.sad_emoji);
............
@Override
public void draw(Canvas canvas) {
FirebaseVisionFace face = firebaseVisionFace;
if (face == null) {
return;
}
Rect boundingBox = face.getBoundingBox();
float smilingProbability = face.getSmilingProbability();
Log.d("3llomi", "smiling " + smilingProbability);
Bitmap emojiToDraw;
if (smilingProbability > 0.175f) {
emojiToDraw = happyEmoji;
} else {
emojiToDraw = sadEmoji;
}
float x = translateX(face.getBoundingBox().centerX());
float y = translateY(face.getBoundingBox().centerY());
float xOffset = scaleX(face.getBoundingBox().width() / 2.0f);
float yOffset = scaleY(face.getBoundingBox().height() / 2.0f);
float left = x - xOffset;
float top = y - yOffset;
float right = x + xOffset;
float bottom = y + yOffset;
canvas.drawRect(left, top, right, bottom, boxPaint);
RectF rectF = new RectF(left,top,right,bottom);
canvas.drawBitmap(emojiToDraw, boundingBox, rectF, bitmapPaint);
}
المشروع على Github
بعض المصادر
التعليقات (0)
لايوجد لديك حساب في عالم البرمجة؟
تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !