En el anterior post sobre el procesamiento de imagen analizamos la manipulación de valores para aplicar diferentes filtros a imágenes que cargábamos en nuestra App. Otro uso muy extendido en muchas aplicaciones es el reconocimiento facial y la detección de puntos concretos. A continuación, veremos cómo hacerlo.
API-Mobile-Vision

Snapchat, Instagram, Facebook,… muchas aplicaciones utilizan el reconocimiento facial o detección de caras y facciones en tiempo real para poder hacer manipulaciones. Posicionar elementos sobre la imagen, retocar ciertos puntos de la imagen para mejorar las facciones, detectar movimientos del usuario (levantar las cejas por ejemplo) para iniciar animaciones, … Todas estas modificaciones tienen un éxito bestial, ya sea porque son útiles pero sobre todo porque son divertidos.

Si la aplicación da pie a ello, añadir este tipo de funcionalidades a nuestro proyecto traería consigo el aumento de su uso.  Eso sí, siempre y cuando la implementación esté correctamente realizada, ya que este tipo de funcionalidades son mejores dejarlas a un lado si no funcionan como deben.

API-Mobile-Vision

Hasta ahora hemos realizado ejemplos utilizando Cordova, pero en este caso no será así. Existen librerías de terceros pensadas para este tipo de menesteres (ezRa o CraftAR) pero la mayoría exigen registrarse para poder publicar apps. Aprovecharemos que Google pone a nuestra disposición la API Mobile Vision para estos menesteres. Esta API ofrece detecciones relacionadas con códigos de barras y caras. En el ámbito del reconocimiento facial, que es el que nos interesa, tenemos las siguientes opciones:

  • Reconocimiento facial: Determinar si dos caras son de la misma persona o no.
  • Seguimiento facial: Se puede hacer seguimiento de caras en un video o señal en directo.
  • Detección de landmarks: Son puntos de interés que podemos encontrar en una cara: ojos, barbilla, nariz, etc…
  • Clasificación: Permite determinar si una cara está sonriendo o no, si los ojos están cerrados o no, etc…

A continuación, veremos cómo crear una aplicación que realiza dos acciones. Por una parte, detectará caras utilizando la cámara frontal y por otro lado detectará los landmarks en diversas fotos. Para este desarrollo utilizaremos Android Studio y necesitaremos un dispositivo en el que poder hacer las pruebas. Se presupone que el desarrollador tiene unos conocimientos básicos sobre programación en Android Studio para aligerar el tutorial. ¡Empecemos!

Creación del proyecto y las actividades

  1. Creamos un nuevo proyecto con el nombre Reconocimiento. Este proyecto estará compuesto por tres actividades:
    • MainActivity: la actividad inicial que contendrá dos botones. Estos botones se utilizarán para navegar a las dos siguientes.
    • DetectionActivity: en esta actividad detectaremos los landmarks en una foto.
    • RecognitionActivity: actividad en la que se detectará y realizará seguimiento de caras.
  2. Añadir la navegación correspondiente para ir de una actividad a otra.
  3. Descargar el material que necesitamos desde este enlace. Dentro encontraréis clases y recursos que utilizaremos.
  4. Copiamos la carpeta raw y machacamos el strings.xml dentro de la carpeta res.

Detección de landmarks

A partir de este punto empezaremos a utilizar las librerías que ofrece Google. Muchas de las clases que se utilizarán en el proyecto están creadas por lo que explicaremos su funcionalidad sin entrar en detalle.

  1. Abrimos el gradle de la app y en la parte de las dependencias añadimos estas dos líneas y sincronizamos:
    compile 'com.google.android.gms:play-services-vision:9.4.0'
    compile 'com.android.support:design:24.2.0'
    

    El primero es la librería para utilizar las funcionalidades de detección y reconocimiento y la segunda la utilizaremos para mostrar mensajes en caso de que no haya permisos.

  2. Añadimos en el Manifest los permisos que vamos a utilizar.
    <uses-feature android:name="android.hardware.camera" />
    <uses-permission android:name="android.permission.CAMERA" />
    
  3. Dentro de la carpeta java, y debajo de nuestro proyecto añadimos un nuevo Package llamado patch y añadimos la clase SafeFaceDetector (esta en los materiales).
  4. Dentro de nuestro proyecto, en la misma raíz si quereís, creamos una nueva clase llamada FaceView. El contenido de esta clase lo tenéis en los materiales. Esta clase es una vista que utilizaremos para cargar la imagen que deseamos analizar y en la cual dibujaremos los landmarks. Le pasaremos como atributos la imagen que deseamos analizar y un array con las caras detectadas.
  5. En el layout relativo de la actividad Recognition copiamos el contenido que tenemos en el xml activity_detection.xml. Como podéis observar, el layout tiene la vista FaceView que acabamos de crear.
  6. En el DetectionActivity añadimos la siguiente variable:
    private static final String TAG = "DetectionActivity";
    
  7. Dentro del método onCreate del DetectionActivity añadimos las siguientes líneas:
    InputStream stream = getResources().openRawResource(R.raw.face2);
     Bitmap bitmap = BitmapFactory.decodeStream(stream);
     
     FaceDetector detector = new FaceDetector.Builder(getApplicationContext())
             .setTrackingEnabled(false)
             .setLandmarkType(FaceDetector.ALL_LANDMARKS)
             .build();
     Detector<Face> safeDetector = new SafeFaceDetector(detector);
     Frame frame = new Frame.Builder().setBitmap(bitmap).build();
     SparseArray<Face> faces = safeDetector.detect(frame);
     
     if (!safeDetector.isOperational()) {
             Log.w(TAG, "Face detector dependencies are not yet available.");
     
             IntentFilter lowstorageFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW);
         boolean hasLowStorage = registerReceiver(null, lowstorageFilter) != null;
     
         if (hasLowStorage) {
             Toast.makeText(this, R.string.low_storage_error, Toast.LENGTH_LONG).show();
             Log.w(TAG, getString(R.string.low_storage_error));
         }
     }
     
     FaceView overlay = (FaceView) findViewById(R.id.faceView);
     overlay.setContent(bitmap, faces);
     
     safeDetector.release();
    

    Lo primero que hacemos es crear un bitmap partiendo de la imagen que queremos analizar. En nuestro caso es el recurso face2.
    Generamos un detector de caras con los atributos que nos interesan. En este caso queremos que detecte los landmarks pero que no lo haga continuamente.
    Partiendo del SafeFaceDetector que hemos añadido creamos el detector. Este SafeDetector se utiliza para evitar un posible bug con imágenes pequeñas. Por ahora lo utilizaremos de esta forma, aunque se supone que Google solucionará este problema en versiones venideras.
    Partiendo del bitmap anteriormente generado creamos un objeto de tipo Frame que analizará el detector.
    El detector detecta las caras en el frame y devuelve un array con los resultados.
    Vemos si el detector está operativo, esto se debe a que, en la primera utilización, el detector debe descargar ciertas librerías para funcionar correctamente. En caso de que no esté operativo muestra un mensaje de aviso y si no hay suficiente espacio para guardarlo muestra otro.
    Obtiene la vista del layout y le pasa los contenidos (la imagen y el array de caras). La misma vista se encargará de dibujar los resultados.
    Una vez realizada la operación liberamos el detector para que se pueda utilizar en otras detecciones.

  8. Ejecutamos la parte de la aplicación relativa y vemos cómo posiciona los elementos encima de la imagen.

Reconocimiento

Una vez realizada la detección en imágenes pasaremos a detectar caras en imagen de video.

  1. Dentro de nuestro proyecto, en la carpeta java, añadimos un nuevo package llamado camera.
  2. Copiamos las clases CameraSourcePreview y GraphicOverlay. La primera es una clase encargada de mostrar la señal de la cámara en la pantalla y la segunda se encargará de mostrar los contenidos encima de la primera. Son clases ya hechas que vamos a reutilizar.
  3. Abrimos el layout llamado activity_recognition.xml y reemplazamos el contenido proporcionado en los materiales. Al final, introducimos las vistas que hemos añadido en el paso anterior.
  4. Añadimos una nueva clase en nuestro proyecto llamada FaceGraphic. Copiad el contenido de esta clase desde los materiales. Esta clase se encargará de dibujar elementos gráficos en el graphic overlay basándose en la información de las caras que reciba.
  5. Abrimos el RecognitionActivity y añadimos las siguientes variables:
    private static final String TAG = "FaceTracker";
    private CameraSource mCameraSource = null;
    private CameraSourcePreview mPreview;
    private GraphicOverlay mGraphicOverlay;
    private static final int RC_HANDLE_GMS = 9001;
    private static final int RC_HANDLE_CAMERA_PERM = 2;
    

    Los utilizaremos para el acceso a la cámara y los objetos visuales.

  6. En el método onCreate añadimos este código:
    mPreview = (CameraSourcePreview) findViewById(R.id.facePreview);
    mGraphicOverlay = (GraphicOverlay) findViewById(R.id.faceOverlay);
    int rc = ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
     if (rc == PackageManager.PERMISSION_GRANTED) {
         createCameraSource();
     } else {
         requestCameraPermission();
     }
    

    Básicamente obtenemos los elementos del layout y verificamos que temenos permisos. En caso afirmativo inicializamos la cámara y en caso negativo pedimos los permisos correspondientes.

  7. Añadimos el código correspondiente a los permisos.
    private void requestCameraPermission() {
        Log.w(TAG, "Camera permission is not granted. Requesting permission");
    
        final String[] permissions = new String[]{Manifest.permission.CAMERA};
    
        if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
                Manifest.permission.CAMERA)) {
            ActivityCompat.requestPermissions(this, permissions, RC_HANDLE_CAMERA_PERM);
            return;
        }
    
        final Activity thisActivity = this;
    
        View.OnClickListener listener = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ActivityCompat.requestPermissions(thisActivity, permissions,
                        RC_HANDLE_CAMERA_PERM);
            }
        };
    
        Snackbar.make(mGraphicOverlay, R.string.permission_camera_rationale,
                Snackbar.LENGTH_INDEFINITE)
                .setAction(R.string.ok, listener)
                .show();
    }
    
    
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode != RC_HANDLE_CAMERA_PERM) {
            Log.d(TAG, "Got unexpected permission result: " + requestCode);
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            return;
        }
    
        if (grantResults.length != 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            Log.d(TAG, "Camera permission granted - initialize the camera source");
            // we have permission, so create the camerasource
            createCameraSource();
            return;
        }
    
        Log.e(TAG, "Permission not granted: results len = " + grantResults.length +
                " Result code = " + (grantResults.length > 0 ? grantResults[0] : "(empty)"));
    
        DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
                finish();
            }
        };
    
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Face Tracker sample")
                .setMessage(R.string.no_camera_permission)
                .setPositiveButton(R.string.ok, listener)
                .show();
    }
    

    El primer método accede a los permisos de Manifest y hace la petición. La segunda es la que analizará la respuesta y en base a la misma hará una cosa u otra.

  8. Añadimos las funcionalidades para crear e inicializar la cámara:
    private void createCameraSource() {
    
        Context context = getApplicationContext();
        FaceDetector detector = new FaceDetector.Builder(context)
                .setClassificationType(FaceDetector.ALL_CLASSIFICATIONS)
                .build();
    
        detector.setProcessor(
                new MultiProcessor.Builder<>(new GraphicFaceTrackerFactory())
                        .build());
    
        if (!detector.isOperational()) {
      Log.w(TAG, "Face detector dependencies are not yet available.");
        }
    
        mCameraSource = new CameraSource.Builder(context, detector)
                .setRequestedPreviewSize(640, 480)
                .setFacing(CameraSource.CAMERA_FACING_FRONT)
                .setRequestedFps(30.0f)
                .build();
    }
    

    En nuestro caso inicializaremos la cámara frontal.

     private void startCameraSource() {
     
         // check that the device has play services available.
         int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(
                 getApplicationContext());
         if (code != ConnectionResult.SUCCESS) {
             Dialog dlg =
                     GoogleApiAvailability.getInstance().getErrorDialog(this, code, RC_HANDLE_GMS);
             dlg.show();
         }
     
         if (mCameraSource != null) {
             try {
                 mPreview.start(mCameraSource, mGraphicOverlay);
             } catch (IOException e) {
                 Log.e(TAG, "Unable to start camera source.", e);
                 mCameraSource.release();
                 mCameraSource = null;
             }
         }
     }

    En el start verificamos que todo está correctamente e iniciamos la cámara.

  9. Añadimos los métodos onResume, onPause y onDestroy. Estos métodos controlarán el funcionamiento de la cámara. Hay que tener en cuenta que se iniciará en el onResume.
    @Override
    protected void onResume() {
        super.onResume();
    
        startCameraSource();
    }
    @Override
    protected void onPause() {
        super.onPause();
        mPreview.stop();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mCameraSource != null) {
            mCameraSource.release();
        }
    }
    
  10. Añadimos dos clases privadas que se encargarán de dibujar los elementos gráficos de cada cara detectada.
    private class GraphicFaceTrackerFactory implements MultiProcessor.Factory<Face> {
        @Override
        public Tracker<Face> create(Face face) {
            return new GraphicFaceTracker(mGraphicOverlay);
        }
    }
    
    /**
     * Face tracker for each detected individual. This maintains a face graphic within the app's
     * associated face overlay.
     */
    private class GraphicFaceTracker extends Tracker<Face> {
        private GraphicOverlay mOverlay;
        private FaceGraphic mFaceGraphic;
    
        GraphicFaceTracker(GraphicOverlay overlay) {
            mOverlay = overlay;
            mFaceGraphic = new FaceGraphic(overlay);
        }
    
        /**
         * Start tracking the detected face instance within the face overlay.
         */
        @Override
        public void onNewItem(int faceId, Face item) {
            mFaceGraphic.setId(faceId);
        }
    
        /**
         * Update the position/characteristics of the face within the overlay.
         */
        @Override
        public void onUpdate(FaceDetector.Detections<Face> detectionResults, Face face) {
            mOverlay.add(mFaceGraphic);
            mFaceGraphic.updateFace(face);
        }
    
        /**
         * Hide the graphic when the corresponding face was not detected.  This can happen for
         * intermediate frames temporarily (e.g., if the face was momentarily blocked from
         * view).
         */
        @Override
        public void onMissing(FaceDetector.Detections<Face> detectionResults) {
            mOverlay.remove(mFaceGraphic);
        }
    
        /**
         * Called when the face is assumed to be gone for good. Remove the graphic annotation from
         * the overlay.
         */
        @Override
        public void onDone() {
            mOverlay.remove(mFaceGraphic);
        }
    }
    
  11. Ejecutad y verificad que detecta las caras con rectángulos y hace el seguimiento.

 

Podéis descargaros el proyecto desde aquí.

Este pequeño tutorial simplemente reutiliza el código proporcionado por Google para la detección, pero nos ha servido como introducción. En el siguiente post unificaremos ambos ejemplos y crearemos nuestra app de realidad aumentada, igual-igual que Facebook o Instagram.