זיהוי פנים - 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:
הקובץ השני עושה את עבודת זיהוי הפנים.
נתחיל עם הקובץ הראשון
תפקידו העיקרי להפעיל יצירת ה-view ולהביאו לתצוגה. כשתי שורות קוד יספיקו לכך. הנה ה-class:
- public class MyFaceDetectionActivity extends Activity {
- /** Called when the activity is first created. */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- FaceDetectionView faceView = new FaceDetectionView(this);
- setContentView(faceView);
- }
- }
שורה 6 מעניינת: היא יוצרת view שלא מתוך קובץ xml ומציגה אותו - שורה 7.
ה-view הנ"ל נוצר ב- FaceDetectionView class, שממומש בקובץ השני.
הקובץ השני
- public class FaceDetectionView extends View {
- private static final int NUM_OF_FACES = 5;
- private static final int CIRCLE_STROKE_WIDTH = 5;
- private FaceDetector faceDetector;
- private FaceDetector.Face detectedFaces[] = new FaceDetector.Face[NUM_OF_FACES];
- private PointF midPoint[] = new PointF[NUM_OF_FACES];
- private float eyesDistance[] = new float[NUM_OF_FACES];
- private Bitmap bitmapImg;
- private Paint paintEye = new Paint(Paint.ANTI_ALIAS_FLAG);
- private int imageWidth, imageHeight;
- private float widthNormalize, heightNormalize;
- int numOfFacesDetected;
- public FaceDetectionView(Context context) {
- super(context);
- /* Convert to bitmap */
- BitmapFactory.Options bitMapFactory = new BitmapFactory.Options();
- bitMapFactory.inPreferredConfig = Bitmap.Config.RGB_565;
- bitmapImg = BitmapFactory.decodeResource( getResources() ,R.drawable.p200, bitMapFactory);
- /* Face Detect */
- imageWidth = bitmapImg.getWidth();
- imageHeight = bitmapImg.getHeight();
- faceDetector = new FaceDetector( imageWidth, imageHeight, NUM_OF_FACES );
- numOfFacesDetected = faceDetector.findFaces(bitmapImg, detectedFaces);
- /* Prepare the paint eliptic eye mark. */
- paintEye.setColor(Color.YELLOW);
- paintEye.setStyle(Paint.Style.STROKE);
- /* extract the face detection results */
- for (int indx = 0; indx < numOfFacesDetected; indx++)
- {
- PointF eyesMidPointTmp = new PointF();
- detectedFaces[indx].getMidPoint(eyesMidPointTmp);
- midPoint[indx] = eyesMidPointTmp;
- eyesDistance[indx] = detectedFaces[indx].eyesDistance();
- }
- }
- @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) { - /* prepare for onDraw */
- widthNormalize = getWidth() *1.0f/ imageWidth;
- heightNormalize = getHeight()*1.0f/ imageHeight;
- super.onSizeChanged(w, h, oldw, oldh);
}
@Override- protected void onDraw(Canvas canvas)
- {
- canvas.drawBitmap( bitmapImg, null , new Rect(0,0,getWidth(),getHeight()),null);
- canvas.drawText("number of detected faces=" + numOfFacesDetected, 100, 80, paintEye);
- canvas.drawText("Info on Face 1 only. Eye dist="+ eyesDistance[0],100, 100,paintEye);
- canvas.drawText("Middle Point=" + midPoint[0].x + "," + midPoint[0].y,100,120,paintEye);
- canvas.drawText("Confidence Factor= " + detectedFaces[0].confidence(),100,140,paintEye);
- for (int indx = 0; indx < numOfFacesDetected; indx++)
- {
- float normalX = midPoint[indx].x * widthNormalize;
- float normalY = midPoint[indx].y * heightNormalize;
- paintEye.setStrokeWidth(CIRCLE_STROKE_WIDTH);
- RectF rect = new RectF(normalX - eyesDistance[indx]/2,
- normalY - eyesDistance[indx]/4,
- normalX + eyesDistance[indx]/2,
- normalY + eyesDistance[indx]/4 );
- canvas.drawOval(rect,paintEye );
- }
- }
- }
אפשר לזהות שלוש מתודות:
ה-constructor שורה 15: שם מתבצע זיהוי הפנים והוצאת הנתונים הקשורה אליו.
onDraw - שורה 45 - שם מתבצע ציור האליפסה סביב הפנים והוספת הכיתוב על גבי התמונה.
onSizeChanged - שורה 39- מתודה קטנה עליה עשיתי override. חישוב גורם הנירמול בין גודל התמונה לגודל התצוגה.
onSizeChanged - שורה 39- מתודה קטנה עליה עשיתי override. חישוב גורם הנירמול בין גודל התמונה לגודל התצוגה.
נתחיל עם ה-constructor שם מתבצעת עיקר הפעילות.
הנה הקוד שוב:
- public FaceDetectionView(Context context) {
- super(context);
- /* Convert to bitmap */
- BitmapFactory.Options bitMapFactoryOptions = new BitmapFactory.Options();
- bitMapFactoryOptions.inPreferredConfig = Bitmap.Config.RGB_565;
- bitmapImg = BitmapFactory.decodeResource( getResources() ,R.drawable.p200, bitMapFactoryOptions);
- /* Face Detect */
- imageWidth = bitmapImg.getWidth();
- imageHeight = bitmapImg.getHeight();
- faceDetector = new FaceDetector( imageWidth, imageHeight, NUM_OF_FACES );
- numOfFacesDetected = faceDetector.findFaces(bitmapImg, detectedFaces);
- /* Prepare the paint for the eliptic eye mark. */
- paintEye.setColor(Color.YELLOW);
- paintEye.setStyle(Paint.Style.STROKE);
- /* extract the face detection results */
- for (int indx = 0; indx < numOfFacesDetected; indx++)
- {
- PointF eyesMidPointTmp = new PointF();
- detectedFaces[indx].getMidPoint(eyesMidPointTmp);
- midPoint[indx] = eyesMidPointTmp;
- eyesDistance[indx] = detectedFaces[indx].eyesDistance();
- }
- }
המתודה מכילה 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: למטה: שליפת המרחק בין העיניים.
- detectedFaces[indx].getMidPoint(eyesMidPointTmp);
- midPoint[indx] = eyesMidPointTmp;
- eyesDistance[indx] = detectedFaces[indx].eyesDistance();
נשתמש בפרמטרים האלה ב-onDraw לציור האליפסה.
אחלה. לפני ה-onDraw עוד מתודה קטנה נוספת שמחשבת את מקדם הנירמול בין גודל התמונה לגודל המסך. היה אפשר לשים את זה ב-onDraw וזה היה בהחלט עובד, אבל עדיף לשים את זה כאן, ב-callback שמופעל עם כל שינוי גודל התצוגה.
- @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) { - /* prepare for onDraw */
- widthNormalize = getWidth() *1.0f/ imageWidth;
- heightNormalize = getHeight()*1.0f/ imageHeight;
- super.onSizeChanged(w, h, oldw, oldh);
}
שורות 3 ו-4 מחשבות את יחס הגדלים עבור הרוחב והגובה בהתאמה. נשתמש בזה כדי לנרמל את מקום העיניים כשנצייר את האליפסה מסביב.
שורה 5 מפעילה את המתודה של ה superclass - חובה כמובן.
הגענו לחלק האחרון של ה-class - המתודה onDraw.
זהו ה-callback שמופעל כשיש שינוי במצב התצוגה.
גם הוא מתחלק לחלקים, הפעם לשלושה חלקים עיקריים.
החלק הראשון שורה 4: ציור התמונה.
החלק השני, שורות 5-8: כתיבת הטקסט על התמונה.
החלק השלישי, שורות 9-19: ציור האליפסה מסביב לעיניים.
- @Override
- protected void onDraw(Canvas canvas)
- {
- canvas.drawBitmap( bitmapImg, null , new Rect(0,0,getWidth(),getHeight()),null);
- canvas.drawText("number of detected faces=" + numOfFacesDetected, 100, 80, paintEye);
- canvas.drawText("Info on Face 1 only. Eye dist="+ eyesDistance[0],100, 100,paintEye);
- canvas.drawText("Middle Point=" + midPoint[0].x + "," + midPoint[0].y,100,120,paintEye);
- canvas.drawText("Confidence Factor= " + detectedFaces[0].confidence(),100,140,paintEye);
- for (int indx = 0; indx < numOfFacesDetected; indx++)
- {
- float normalX = midPoint[indx].x * widthNormalize;
- float normalY = midPoint[indx].y * heightNormalize;
- paintEye.setStrokeWidth(CIRCLE_STROKE_WIDTH);
- RectF rect = new RectF(normalX - eyesDistance[indx]/2,
- normalY - eyesDistance[indx]/4,
- normalX + eyesDistance[indx]/2,
- normalY + eyesDistance[indx]/4 );
- canvas.drawOval(rect,paintEye );
- }
- }
- }
לגבי ציור התמונה, שורה 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: הוא נוצר ואותחל למעלה, מחוץ למתודה.הנה השורה:
עוד הערה לגבי האובייקט paintEye: הוא נוצר ואותחל למעלה, מחוץ למתודה.הנה השורה:
private Paint paintEye = new Paint(Paint.ANTI_ALIAS_FLAG);
הפרמטר ANTI_ALIAS_FLAG נותן תצוגה חלקה של תוצרי ה-draw.
איך ניתן לבצע זיהוי פנים ב LIVE, בלי לשלוח קבצי תמונות אלא מהמצלמה עצמה ?
השבמחקכמו במצלמה של HD2 שמזהה ב LIVE
הי תומר, עדין לא מימשתי זהוי פנים באופן זה, אבל התהליך צריך להשתמש ב- 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
היי רונן
השבמחקקודם כל תודה על הפוסט הזה והעזרה בכלל,
עשיתי את השלבים שציינת אבל עכשיו אני תקוע בבעיה אחרת.
מצד אחד יש לי את ה- VIEW שעליו אני מציג את הפריימים מהמצלמה לפני שאני עושה CAPTURE
ומצד שני יש לי CANVAS שעליו אני רוצה להציג את ה- FRAME לאחר שבוצע עליו זיהוי פנים
הבעיה איך אני משלב בין השנים בצורה חלקה ?
הי תומר,
השבמחקאני מציע לבטל את הצגת הפריימים מהמצלמה ולהציג רק את התוצאה לאחר הזיהוי.
נסתכל על הקוד בדף של ה-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.
כרגיל, אני מחכה לשמוע את התוצאות.