Firebase MLKit التعرف على الوجوه

AbdulAlim Rajjoubمنذ 5 سنوات

شرحنا سابقاً درس التعرف على النصوص باستخدام 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 ونعطيها عنصر الوجه و  وحجم الدائرة 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

 

بعض المصادر 

1 2 3

3
إعجاب
3677
مشاهدات
0
مشاركة
2
متابع
متميز
محتوى رهيب

التعليقات (0)

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !