יום שלישי, דצמבר 14

זהוי פנים

זיהוי פנים - Face Detection. יש לאנדרואיד class כזה. אבל איך זה עובד?? בדקתי בשבילכם. אכן יש class בשם FaceDetector.
  • הוא מסוגל לזהות פנים מתמונות בפורמט bmp. 
  • הוא מכיל מתודה אחת לזיהוי פנים, שמחזירה גם את מספר הפרצופים שזוהו בתמונה.
  • הוא מסוגל לזהות עד 64 פרצופים בתמונה. ה-class שמקונן בו (nested class) - בשם FaceDetector.Face כולל את 4 המתודות הבאות:
  • Confidence - מחזירה פאקטור בין 0-1 המציין את רמת הביטחון בזיהוי. CONFIDENCE_THRESHOLD - הסף לזיהוי מוצלח נקבע על 0.4.
  •  eyesDistance - מחזירה את המרחק בין העיניים.
  •  getMidPoint- מחזירה את הקואורדינטות של נקודת המרכז בין העייניים.
  •  pose- מחזירה את זווית הפנים. (אני מקבל תמיד 0, כך שנראה לי שז לא עובד, לפחות ב 2.2).

אז מדובר בסה"כ ב-5 מתודות שיעניינו אותנו: אחת לזיהוי הפנים ועוד 4 (מתוכן רק 3 "עובדות") להוצאת פרמטרים.
לא חקרתי כיצד מתבצע זהוי הפנים, אבל 4 המתודות הנ"ל מאפשרות שליפת נתונים המתייחסים רק לעיינים. האם זה אומר שרק רק העיניים קשורות לתהליך הזהוי?
ניסקור מייד את האפליקציה הקטנה שהכנתי. הרצתי אותה על תמונה, והנה התוצאה:
תמונה 1: FaceDetection




 על גבי התמונה מצוירת אליפסה סביב איזור העייניים ומופיע טקסט המתאר את הפרמטרים שהתקבלו מהתמונה ע"י המתודות של FaceDetector ו-FaceDetector.Face -ראה למעלה. 

נעיף מבט על הקוד, ונתייחס לשני קבצי ה-ג'אווה:

הקובץ הראשון, מכיל את ה-Activity הראשי. מכיל class קטן בלי הרבה בשר.
הקובץ השני עושה את עבודת זיהוי הפנים.

נתחיל עם הקובץ הראשון 
תפקידו העיקרי להפעיל יצירת ה-view ולהביאו לתצוגה. כשתי שורות קוד יספיקו לכך. הנה ה-class:



  1. public class MyFaceDetectionActivity extends Activity {
  2.      /** Called when the activity is first created. */
  3.          @Override
  4.          public void onCreate(Bundle savedInstanceState) {
  5.         super.onCreate(savedInstanceState);
  6.         FaceDetectionView faceView = new FaceDetectionView(this);
  7.         setContentView(faceView);
  8.      }
  9.  }

שורה 6 מעניינת: היא יוצרת view שלא מתוך קובץ xml ומציגה אותו - שורה 7.
ה-view  הנ"ל נוצר ב- FaceDetectionView class, שממומש בקובץ השני.

הקובץ השני

  1. public class FaceDetectionView extends View {
  2.            private static final int NUM_OF_FACES = 5;
  3.            private static final    int CIRCLE_STROKE_WIDTH = 5;
  4.            private FaceDetector faceDetector;
  5.            private FaceDetector.Face detectedFaces[] = new FaceDetector.Face[NUM_OF_FACES];
  6.            private PointF     midPoint[] = new PointF[NUM_OF_FACES];
  7.            private float      eyesDistance[] = new float[NUM_OF_FACES];
  8.            private Bitmap    bitmapImg;
  9.            private Paint     paintEye = new Paint(Paint.ANTI_ALIAS_FLAG);
  10.            private int         imageWidth, imageHeight;
  11.            private float     widthNormalize, heightNormalize;
  12.            int numOfFacesDetected;
  13.          
  14.    
  15.            public FaceDetectionView(Context context) {
  16.                    super(context);
  17.                    /* Convert to bitmap */                  
  18.                    BitmapFactory.Options bitMapFactory = new BitmapFactory.Options();
  19.                    bitMapFactory.inPreferredConfig = Bitmap.Config.RGB_565;
  20.                    bitmapImg = BitmapFactory.decodeResource( getResources() ,R.drawable.p200, bitMapFactory);
  21.                       /* Face Detect */
  22.                    imageWidth = bitmapImg.getWidth();
  23.                    imageHeight = bitmapImg.getHeight();
  24.  
  25.                    faceDetector = new FaceDetector( imageWidth, imageHeight, NUM_OF_FACES );
  26.                    numOfFacesDetected = faceDetector.findFaces(bitmapImg, detectedFaces);
  27.                    /* Prepare the paint eliptic eye mark.  */   
  28.                    paintEye.setColor(Color.YELLOW);
  29.                    paintEye.setStyle(Paint.Style.STROKE);
  30.                  /* extract the face detection results */
  31.                    for (int indx = 0; indx < numOfFacesDetected; indx++)
  32.                    {
  33.                            PointF eyesMidPointTmp = new PointF();
  34.                                    detectedFaces[indx].getMidPoint(eyesMidPointTmp);
  35.                                    midPoint[indx] = eyesMidPointTmp;
  36.                                    eyesDistance[indx] = detectedFaces[indx].eyesDistance();
  37.                     }
  38.         }
  39.            @Override
               protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  40.   /* prepare for onDraw */
  41.                   widthNormalize = getWidth() *1.0f/ imageWidth;
  42.                    heightNormalize = getHeight()*1.0f/ imageHeight;
  43.                    super.onSizeChanged(w, h, oldw, oldh);
                }

  44.             @Override
  45.            protected void onDraw(Canvas canvas)
  46.            {
  47.                    canvas.drawBitmap( bitmapImg, null , new Rect(0,0,getWidth(),getHeight()),null);
  48.                 canvas.drawText("number of detected faces=" + numOfFacesDetected, 100, 80, paintEye);
  49.                 canvas.drawText("Info on Face 1 only. Eye dist="+ eyesDistance[0],100, 100,paintEye);
  50.                 canvas.drawText("Middle Point=" +  midPoint[0].x + "," +  midPoint[0].y,100,120,paintEye);
  51.                 canvas.drawText("Confidence Factor= " +  detectedFaces[0].confidence(),100,140,paintEye);
  52.                 for (int indx = 0; indx < numOfFacesDetected; indx++)
  53.                 {
  54.                             float normalX = midPoint[indx].x * widthNormalize;
  55.                             float normalY = midPoint[indx].y * heightNormalize;
  56.                             paintEye.setStrokeWidth(CIRCLE_STROKE_WIDTH);
  57.                               RectF rect = new RectF(normalX - eyesDistance[indx]/2,
  58.                                       normalY - eyesDistance[indx]/4,
  59.                                       normalX + eyesDistance[indx]/2,
  60.                                       normalY + eyesDistance[indx]/4 ); 
  61.                               canvas.drawOval(rect,paintEye );
  62.                 }
  63.    
  64.            }
  65.    
  66.     }


אפשר לזהות שלוש מתודות: 
ה-constructor שורה  15: שם מתבצע זיהוי הפנים והוצאת הנתונים הקשורה אליו.
onDraw - שורה 45 - שם מתבצע ציור האליפסה סביב הפנים והוספת הכיתוב על גבי התמונה.
onSizeChanged - שורה 39- מתודה קטנה עליה עשיתי override. חישוב גורם הנירמול בין גודל התמונה לגודל התצוגה.


נתחיל עם ה-constructor שם מתבצעת עיקר הפעילות.



הנה הקוד  שוב:

  1.        public FaceDetectionView(Context context) {
  2.                    super(context);
  3.                    /* Convert to bitmap */                  
  4.                    BitmapFactory.Options bitMapFactoryOptions = new BitmapFactory.Options();
  5.                    bitMapFactoryOptions.inPreferredConfig = Bitmap.Config.RGB_565;
  6.                    bitmapImg = BitmapFactory.decodeResource( getResources() ,R.drawable.p200, bitMapFactoryOptions);
  7.                    /* Face Detect */
  8.                    imageWidth = bitmapImg.getWidth();
  9.                    imageHeight = bitmapImg.getHeight();
  10.                     faceDetector = new FaceDetector( imageWidth, imageHeight, NUM_OF_FACES );
  11.                    numOfFacesDetected = faceDetector.findFaces(bitmapImg, detectedFaces);
  12.                    /* Prepare the paint for the eliptic eye mark.  */   
  13.                    paintEye.setColor(Color.YELLOW);
  14.                    paintEye.setStyle(Paint.Style.STROKE);
  15.        /* extract the face detection results */
  16.                    for (int indx = 0; indx < numOfFacesDetected; indx++)
  17.                    {
  18.                            PointF eyesMidPointTmp = new PointF();
  19.                                    detectedFaces[indx].getMidPoint(eyesMidPointTmp);
  20.                                    midPoint[indx] = eyesMidPointTmp;
  21.                                    eyesDistance[indx] = detectedFaces[indx].eyesDistance();
  22.  
  23.                    }
  24.                }



המתודה מכילה 4 חלקים עיקריים - בראש כל חלק הוספתי הערה  (comment). הקונפיגורציה המומלצת היא RGB_565.

חלק ראשון, שורה 4-6: המרה ל-bitmap, עליו מתבצעת פעולת הזיהוי.
שורה 4 יוצרת אובייקט options, בעזרתו קובעים options עבור ה-bitmap. הפרמטר היחידי אותו נקבע הוא הפורמט RGB_565 שהוא המומלץ עבור זיהוי פנים (שורה 5). לא מצאתי שום הבדל שנוצר עם פורמט זה, אבל "נזרום" עם המלצה.
שורה 6 מבצעת את ה-decoding של התמונה ל-bitmap. שם קובץ התמונה: p200 (תמונה מקרית שמצאתי באינטרנט). אובייקט ה-options נמסר כפרמטר.
חלק שני, שורה 8-11: ביצוע הזיהוי והפעלת מתודות לשליפת תוצאות.
שורה 10 מאתחלת את האובייקט מסוג FaceDetector. הפרמטרים: אורך ורוחב התמונה, ומספר הפרצופים המקסימלי לזהוי, אותו הגדרתי ב NUM_OF_FACES . המקסימום שנתמך הוא 64. כאן שמתי NUM_OF_FACES=5.
 שורה 11 מפעילה את זהוי הפנים. הפרמטרים של המתודה:
bitmapImg - התמונה.
detectedFaces- אובייקט מסוג FaceDetector.Face אליו מועברות תוצאות הזיהוי של כל הפרצופים. אצלנו מדובר במערך בגודל NUM_OF_FACES.
המתודה מחזירה את מספר הפרצופים שזוהו.
חלק שלישי: שורה 13-14. הכנת ה-paint לציור האליפסה. הפעולה מתבצעת כאן, אך היתה יכולה להתבצע גם  ב- -onDraw.
חלק רביעי: שורות 16-23. שליפת תוצאות הזיהוי מכל אחד מהפרצופים. לצורך זה מתבצע loop על כל הפרצופים שזוהו.
בתוך הלופ שולפים 2 פרמטרים (השורות הועתקו שוב הנה...).
שורה 1 למטה: שליפת נקודת האמצע בין העייניים
שורה 3: למטה: שליפת המרחק בין העיניים.
  1.       detectedFaces[indx].getMidPoint(eyesMidPointTmp);
  2.                                    midPoint[indx] = eyesMidPointTmp;
  3.                                    eyesDistance[indx] = detectedFaces[indx].eyesDistance();


נשתמש בפרמטרים האלה ב-onDraw לציור האליפסה.

אחלה. לפני ה-onDraw עוד מתודה קטנה נוספת שמחשבת את מקדם הנירמול בין גודל התמונה לגודל המסך. היה אפשר לשים את זה ב-onDraw וזה היה בהחלט עובד, אבל עדיף לשים את זה כאן, ב-callback שמופעל עם כל שינוי גודל התצוגה.

  1.       @Override
               protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  2.   /* prepare for onDraw */
  3.                   widthNormalize = getWidth() *1.0f/ imageWidth;
  4.                    heightNormalize = getHeight()*1.0f/ imageHeight;
  5.                    super.onSizeChanged(w, h, oldw, oldh);
                }
שורות 3 ו-4 מחשבות את יחס הגדלים עבור הרוחב והגובה בהתאמה. נשתמש בזה כדי לנרמל את מקום העיניים כשנצייר את האליפסה מסביב.
שורה 5 מפעילה את המתודה של ה superclass - חובה כמובן.

הגענו לחלק האחרון של ה-class - המתודה onDraw.

זהו ה-callback שמופעל כשיש שינוי במצב התצוגה.

גם הוא מתחלק לחלקים, הפעם לשלושה חלקים עיקריים.
החלק הראשון שורה 4: ציור התמונה.
החלק השני, שורות 5-8: כתיבת הטקסט על התמונה.
החלק השלישי, שורות 9-19:  ציור האליפסה מסביב לעיניים.



  1.     @Override
  2.            protected void onDraw(Canvas canvas)
  3.            {
  4.                    canvas.drawBitmap( bitmapImg, null , new Rect(0,0,getWidth(),getHeight()),null);
  5.                 canvas.drawText("number of detected faces=" + numOfFacesDetected, 100, 80, paintEye);
  6.                 canvas.drawText("Info on Face 1 only. Eye dist="+ eyesDistance[0],100, 100,paintEye);
  7.                 canvas.drawText("Middle Point=" +  midPoint[0].x + "," +  midPoint[0].y,100,120,paintEye);
  8.                 canvas.drawText("Confidence Factor= " +  detectedFaces[0].confidence(),100,140,paintEye);
  9.                 for (int indx = 0; indx < numOfFacesDetected; indx++)
  10.                 {
  11.                             float normalX = midPoint[indx].x * widthNormalize;
  12.                             float normalY = midPoint[indx].y * heightNormalize;
  13.                             paintEye.setStrokeWidth(CIRCLE_STROKE_WIDTH);
  14.                               RectF rect = new RectF(normalX - eyesDistance[indx]/2,
  15.                                       normalY - eyesDistance[indx]/4,
  16.                                       normalX + eyesDistance[indx]/2,
  17.                                       normalY + eyesDistance[indx]/4 ); 
  18.                               canvas.drawOval(rect,paintEye );
  19.                 }
  20.    
  21.            }
  22.    
  23.     }

לגבי ציור התמונה, שורה 4:
canvas.drawBitmap( bitmapImg, null , new Rect(0,0,getWidth(),getHeight()),null);

הנה הפרוטוטייפ של המתודה:
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) 

  Rect src לא רלוונטי כאן.
Rect dst: מיוצר כאן. 
paint - לא רלוונטי כאן.


החלק השני - כתיבת הטקסט. כאן הטקסט מתייחס רק לפרצוף הראשון שזוהה.
החלק השלישי - ציור האליפסה.פשוט לגמרי, רק נצביע על נירמול הקואורדינטות של מרכז העיניים. הקואורדינטות התייחסו לקובץ ה-bitmap אבל הוא "נמתח" בהתאם לגודל ה-view.
עוד הערה לגבי האובייקט paintEye: הוא נוצר ואותחל למעלה, מחוץ למתודה.הנה השורה:

        private Paint     paintEye = new Paint(Paint.ANTI_ALIAS_FLAG);
 
הפרמטר ANTI_ALIAS_FLAG נותן תצוגה חלקה של תוצרי ה-draw.



4 תגובות:

  1. איך ניתן לבצע זיהוי פנים ב LIVE, בלי לשלוח קבצי תמונות אלא מהמצלמה עצמה ?

    כמו במצלמה של HD2 שמזהה ב LIVE

    השבמחק
  2. הי תומר, עדין לא מימשתי זהוי פנים באופן זה, אבל התהליך צריך להשתמש ב- previewCallback וזה לפחות לכאורה פשוט:
    1. צור אובייקט של Camera - ראה קישור למטה.
    2. הגדר את ה-previewCallback בעזרת המתודה:
    setOneShotPreviewCallback (Camera.PreviewCallback cb) - ראה בקישור הנ"ל.
    3. הגדר את ה-preview format להיות RGB565 בעזרת המתודה: setPreviewFormat. תמצא שם גם אותה.
    4. בנה את Camera.PreviewCallback. צריך לממש שם את המתודה: onPreviewFrame(byte[] data, Camera camera).
    5. ומכאן אני חושב שאתה יודע איך להמשיך.

    הייתי מריץ את זה בשמחה, אני קצת עסוק כרגע. אשמח לשמוע תוצאות ממך.

    העזר באתר אנדרואיד בהגדרות של ה-classes הנ"ל:
    http://developer.android.com/reference/android/hardware/Camera.Parameters.html#setPreviewFormat(int)

    וגם:http://developer.android.com/reference/android/hardware/Camera.PreviewCallback.html

    השבמחק
  3. היי רונן

    קודם כל תודה על הפוסט הזה והעזרה בכלל,

    עשיתי את השלבים שציינת אבל עכשיו אני תקוע בבעיה אחרת.

    מצד אחד יש לי את ה- VIEW שעליו אני מציג את הפריימים מהמצלמה לפני שאני עושה CAPTURE

    ומצד שני יש לי CANVAS שעליו אני רוצה להציג את ה- FRAME לאחר שבוצע עליו זיהוי פנים


    הבעיה איך אני משלב בין השנים בצורה חלקה ?

    השבמחק
  4. הי תומר,
    אני מציע לבטל את הצגת הפריימים מהמצלמה ולהציג רק את התוצאה לאחר הזיהוי.
    נסתכל על הקוד בדף של ה-Camera class:

    בשלב 5 נאמר:

    Important: Pass a fully initialized SurfaceHolder to setPreviewDisplay(SurfaceHolder). Without a surface, the camera will be unable to start the preview.

    בטל את זה, וגם את שלב 6. כעת ה-Camera לא תשתמש ב-surfaceview.
    כרגיל, אני מחכה לשמוע את התוצאות.

    השבמחק