Identificación de personas con el SDK Snapdragon de Qualcomm
Spanish (Español) translation by CYC (you can also view the original English article)
No fue hace tanto tiempo que tomar fotos era bastante caro. Las cámaras requieren película con capacidad limitada y ver los resultados también requiere tiempo adicional y más dinero. Estas restricciones inherentes aseguraron que fuéramos selectivos con las fotos que tomamos.
Avanzamos rápidamente hasta el día de hoy y estas limitaciones se han visto mermadas gracias a la tecnología, pero ahora nos enfrentamos a un nuevo problema, filtrando, organizando y descubriendo fotos importantes de las muchas que tomamos.
Este nuevo problema es lo que inspiró este tutorial. En él, demostraré cómo podemos usar nuevas herramientas para ayudar a facilitar la vida del usuario mediante la introducción de nuevas formas de filtrar y organizar nuestro contenido.
1. Concepto
Para este proyecto, vamos a ver una forma diferente de filtrar a través de tu colección de fotos. En el camino, aprenderás a integrar y usar el SDK Snapdragon de Qualcomm para el procesamiento y reconocimiento facial.
Permitiremos al usuario filtrar una colección de fotos por identidad/identidades. La colección se filtrará por las identidades de una foto que el usuario toque, como se muestra a continuación.



2. Resumen
El objetivo principal de esta publicación es la introducción del procesamiento facial y el reconocimiento mediante el SDK Snapdragon de Qualcomm, mientras que, con un poco de suerte, fomenta indirectamente nuevas formas de pensar y el uso de metadatos derivados del contenido.
Para evitar quedar obsesionado con la fontanería, he creado una plantilla que proporciona el servicio básico para escanear a través de la colección de fotos del usuario y una cuadrícula para mostrar las fotos. Nuestro objetivo es mejorar esto con el concepto propuesto anteriormente.
En la siguiente sección, revisaremos brevemente estos componentes antes de pasar a la introducción del SDK de Snapdragon de Qualcomm.
3. Esqueleto
Como se mencionó anteriormente, nuestro objetivo es centrarnos en el SDK de Snapdragon, así que he creado un esqueleto que tiene implementadas todas las tuberías. A continuación se muestra un diagrama y una descripción del proyecto, que está disponible para su descarga desde GitHub.



Nuestro paquete de datos contiene una implementación de SQLiteOpenHelper (IdentityGalleryDatabase) responsable de crear y administrar nuestra base de datos. La base de datos constará de tres tablas, una para actuar como un puntero al registro multimedia (photo), otra para las identidades detectadas (identity) y finalmente la tabla de relaciones que conecta las identidades con sus fotos (identity_photo).



Utilizaremos la tabla de identidad para almacenar los atributos proporcionados por el SDK de Snapdragon, detallados en una sección posterior de este tutorial.
También se incluyen en el paquete de datos una clase Provider (IdentityGalleryProvider) y Contract (IdentityGalleryContract), que no es más que un proveedor estándar que actúa como un contenedor de la clase SQLiteOpenHelper.
Para darte una idea de cómo interactuar con la clase Provider, se toma el siguiente código de la clase TestProvider. Como su nombre lo sugiere, se usa para probar la clase de Provider.
1 |
//… Query for all Photos
|
2 |
Cursor cursor = mContext.getContentResolver().query( |
3 |
IdentityGalleryContract.PhotoEntity.CONTENT_URI, |
4 |
null, |
5 |
null, |
6 |
null, |
7 |
null
|
8 |
);
|
9 |
|
10 |
//… Query for all Photos that include any of the identities within the referenced photo
|
11 |
Cursor cursor = mContext.getContentResolver().query( |
12 |
IdentityGalleryContract.PhotoEntity.buildUriWithReferencePhoto(photoId), |
13 |
null, |
14 |
null, |
15 |
null, |
16 |
null
|
17 |
);
|
18 |
|
19 |
//… Query call identities
|
20 |
Cursor cursor = mContext.getContentResolver().query( |
21 |
IdentityGalleryContract.IdentityEntity.CONTENT_URI, |
22 |
null, |
23 |
null, |
24 |
null, |
25 |
null
|
26 |
);
|
27 |
|
28 |
//… Query for all
|
29 |
Cursor cursor = mContext.getContentResolver().query( |
30 |
IdentityGalleryContract.PhotoEntity.CONTENT_URI, |
31 |
null, |
32 |
null, |
33 |
null, |
34 |
null
|
35 |
);
|
El paquete service es responsable de iterar, catalogar y, finalmente, procesar las imágenes disponibles a través de MediaStore. El servicio en sí extiende IntentService como una forma fácil de realizar el procesamiento en su propio hilo. El trabajo real se delega en GalleryScanner, que es la clase que ampliaremos para el procesamiento facial y el reconocimiento.
Este GalleryScannerIntentService se crea una instancia cada vez que se crea MainActivity con la siguiente llamada:
1 |
@Override
|
2 |
protected void onCreate(Bundle savedInstanceState) { |
3 |
...
|
4 |
GalleryScannerIntentService.startActionScan(this.getApplicationContext()); |
5 |
...
|
6 |
}
|
Cuando se inicia, GalleryScannerIntentService obtiene la última fecha de escaneo y la pasa al constructor de GalleryScanner. Luego llama al método scan para comenzar a iterar a través del contenido del proveedor de contenido de MediaItem, para los artículos después de la última fecha de escaneo.
Si inspeccionas el método scan de la clase GalleryScanner, notarás que es bastante detallado: nada complicado está sucediendo aquí. El método necesita consultar los archivos multimedia almacenados internamente (MediaStore.Images.Media.INTERNAL_CONTENT_URI) y externamente (MediaStore.Images.Media.EXTERNAL_CONTENT_URI). Cada elemento se pasa a un método de gancho, que es donde colocaremos nuestro código para el procesamiento facial y el reconocimiento.
1 |
private void processImage(ContentValues contentValues, Uri contentUri) { |
2 |
throw new UnsupportedOperationException("Hook method is not currently implemented"); |
3 |
}
|
Se encuentran disponibles otros dos métodos de enlace en la clase GalleryScanner (como sugieren los nombres de los métodos) para inicializar y des-inicializar la instancia FacialProcessing.
1 |
private void initFacialProcessing() throws UnsupportedOperationException { |
2 |
throw new UnsupportedOperationException("Hook method is not currently implemented"); |
3 |
}
|
4 |
|
5 |
private void deinitFacialProcessing() { |
6 |
throw new UnsupportedOperationException("Hook method is not currently implemented"); |
7 |
}
|
El paquete final es el paquete de presentación. Como su nombre indica, aloja la clase Activity responsable de renderizar nuestra galería. La galería es un GridView conectado a un CursorAdapter. Como se explicó anteriormente, al tocar un elemento consultará en la base de datos las fotos que contengan una de las identidades de la foto seleccionada. Por ejemplo, si tocas una foto de tu amiga Lisa y su novio Justin, la consulta filtrará todas las fotos que contengan una o ambas, Lisa y Justin.
4. SDK Snapdragon de Qualcomm
Para ayudar a los desarrolladores a hacer que su hardware se vea bien y haga justicia, Qualcomm ha lanzado un increíble conjunto de SDK, uno de ellos es el SDK de Snapdragon. Snapdragon SDK expone un conjunto optimizado de funciones para el procesamiento facial.
El SDK se divide ampliamente en dos partes: procesamiento facial y reconocimiento facial. Dado que no todos los dispositivos admiten ambas o ninguna de estas características, que probablemente sea la razón por la que se separan estas características, el SDK proporciona una manera fácil de verificar qué características admite el dispositivo. Cubriremos esto con más detalle más adelante.
El procesamiento facial proporciona una forma de extraer las características de una foto (de una cara), que incluye:
- Detección de parpadeo: Mide qué tan abierto está cada ojo.
- Seguimiento de la mirada: Evalúa dónde mira el sujeto.
- Valor de sonrisa: Estima el grado de la sonrisa.
- Orientación de la cara: Sigue la guiñada, el cabeceo y el balanceo de la cabeza.
El reconocimiento facial, como su nombre lo indica, proporciona la capacidad de identificar personas en una foto. Vale la pena señalar que todo el procesamiento se realiza localmente, a diferencia de la nube.
Estas características se pueden usar en tiempo real (video/cámara) o fuera de línea (galería). En nuestro ejercicio, utilizaremos estas funciones sin conexión, pero existen diferencias mínimas entre los dos enfoques.
Consulta la documentación en línea de los dispositivos compatibles para obtener más información sobre el procesamiento facial y el reconocimiento facial.
5. Agregar procesamiento facial y reconocimiento
En esta sección, completaremos esos métodos de anzuelo (con pocas líneas de código) para brindar a nuestra aplicación la capacidad de extraer propiedades faciales e identificar personas. Para trabajar, descarga la fuente de GitHub y abre el proyecto en Android Studio. Alternativamente, puedes descargar el proyecto completo.
Paso 1: Instalación del SDK de Snapdragon
Lo primero que debemos hacer es tomar el SDK del sitio web de Qualcomm. Ten en cuenta que deberás registrarte/iniciar sesión y aceptar los términos y condiciones de Qualcomm.
Una vez descargado, desarchiva los contenidos y navega a /Snapdragon_sdk_2.3.1/java/libs/libs_facial_processing/. Copia el archivo sd-sdk-facial-processing.jar en la carpeta /app/libs/ de tu proyecto como se muestra a continuación.



Después de copiar el SDK de Snapdragon, haz clic con el botón derecho en sd-sdk-facial-processing.jar y selecciona Agregar como biblioteca... en la lista de opciones.



Esto agregará la biblioteca como una dependencia en tu archivo build.gradle como se muestra a continuación.
1 |
dependencies { |
2 |
compile fileTree(dir: 'libs', include: ['*.jar']) |
3 |
compile files('libs/sd-sdk-facial-processing.jar') |
4 |
compile 'com.android.support:support-v13:20.0.0' |
5 |
}
|
El último paso es agregar la biblioteca nativa. Para ello, crea una carpeta llamada jniLibs en tu carpeta /app/src/main/ y copia la carpeta armeabi (de la descarga del SDK) y su contenido en ella.



Ahora estamos listos para implementar la lógica para identificar a las personas que usan la funcionalidad de la API. Los siguientes fragmentos de código pertenecen a la clase GalleryScanner.
Paso 2: Inicialización
Primero abordemos el método de enganche de inicialización.
1 |
private void initFacialProcessing() throws UnsupportedOperationException{ |
2 |
if( |
3 |
!FacialProcessing.isFeatureSupported(FacialProcessing.FEATURE_LIST.FEATURE_FACIAL_PROCESSING) || !FacialProcessing.isFeatureSupported(FacialProcessing.FEATURE_LIST.FEATURE_FACIAL_RECOGNITION)){ |
4 |
throw new UnsupportedOperationException("Facial Processing or Recognition is not supported on this device"); |
5 |
}
|
6 |
|
7 |
mFacialProcessing = FacialProcessing.getInstance(); |
8 |
if(mFacialProcessing != null){ |
9 |
mFacialProcessing.setRecognitionConfidence(mConfidenceThreshold); |
10 |
mFacialProcessing.setProcessingMode(FacialProcessing.FP_MODES.FP_MODE_STILL); |
11 |
loadAlbum(); |
12 |
} else{ |
13 |
throw new UnsupportedOperationException(“An instance is already in use"); |
14 |
}
|
15 |
}
|
Primero debemos verificar que el dispositivo sea compatible con el procesamiento facial y el reconocimiento facial. Si no es así, lanzamos una excepción UnsupportedOperationException.
Después de eso, asignamos nuestra referencia local de la clase FacialProcessing, mFacialProcessing, a una nueva instancia utilizando el método de fábrica getInstance. Esto devolverá null si una instancia ya está en uso, en cuyo caso se requiere que el consumidor llame a release en esa referencia.
Si hemos obtenido con éxito una instancia de un objeto FacialProcessing, lo configuramos primero configurando la confianza. Hacemos esto usando una variable local, que es 57 en este caso desde un rango de 0 a 100. La confianza es un umbral cuando se trata de resolver identidades. Cualquier coincidencia por debajo de este umbral se considerará como identidades separadas.
En términos de determinar el valor, hasta donde puedo decir, este es un proceso de prueba y error. Obviamente, cuanto más alto es el umbral, más preciso es el reconocimiento, con la compensación de aumentar el número de falsos positivos.
Luego configuramos el modo FacialProcessing en FP_MODE_STILL. Tus opciones aquí son FP_MODE_STILL o FP_MODE_VIDEO. Como sugieren los nombres, uno está optimizado para imágenes fijas mientras que el otro para cuadros continuos, ambos con casos de uso obvios.
P_MODE_STILL, como podrías sospechar, proporciona resultados más precisos. Pero como verás más adelante, FP_MODE_STILL está implícito en el método que usamos para procesar la imagen, por lo que esta línea se puede omitir. Solo lo agregué para completarlo.
Luego llamamos a loadAlbum (método de la clase GalleryScanner), que es lo que veremos a continuación.
1 |
private void loadAlbum(){ |
2 |
SharedPreferences sharedPreferences = mContext.getSharedPreferences(TAG, 0); |
3 |
String arrayOfString = sharedPreferences.getString(KEY_IDENTITY_ALBUM, null); |
4 |
|
5 |
byte[] albumArray = null; |
6 |
if (arrayOfString != null) { |
7 |
String[] splitStringArray = arrayOfString.substring(1, |
8 |
arrayOfString.length() - 1).split(", "); |
9 |
|
10 |
albumArray = new byte[splitStringArray.length]; |
11 |
for (int i = 0; i < splitStringArray.length; i++) { |
12 |
albumArray[i] = Byte.parseByte(splitStringArray[i]); |
13 |
}
|
14 |
mFacialProcessing.deserializeRecognitionAlbum(albumArray); |
15 |
}
|
16 |
}
|
La única línea interesante aquí es:
1 |
mFacialProcessing.deserializeRecognitionAlbum(albumArray); |
Su método contrario es:
1 |
byte[] albumBuffer = mFacialProcessing.serializeRecogntionAlbum(); |
Una sola instancia FacialProcessing puede considerarse como una sesión. Las personas agregadas (explicadas a continuación) se almacenan localmente (denominado "álbum de reconocimiento") dentro de esa instancia. Para permitir que tu álbum persista en varias sesiones, es decir, cada vez que obtienes una nueva instancia, necesitas una forma de persistir y cargarlas.
El método serializeRecogntionAlbum convierte el álbum en una matriz de bytes y, a la inversa, deserializeRecognitionAlbum cargará y analizará un álbum previamente almacenado como una matriz de bytes.
Paso 3: Des-inicialización
Ahora sabemos cómo inicializar la clase FacialProcessing para el procesamiento facial y el reconocimiento. Ahora volvamos nuestro enfoque a la des-inicialización implementando el método deinitFacialProcessing.
1 |
private void deinitFacialProcessing(){ |
2 |
if(mFacialProcessing != null){ |
3 |
saveAlbum(); |
4 |
mFacialProcessing.release(); |
5 |
mFacialProcessing = null; |
6 |
}
|
7 |
}
|
Como se mencionó anteriormente, solo puede haber una instancia de la clase FacialProcessing a la vez, por lo que debemos asegurarnos de liberarla antes de finalizar nuestra tarea. Hacemos esto a través de un método de lanzamiento llamado release. Pero primero hacemos que el álbum de reconocimiento persista para que podamos usar los resultados en varias sesiones. En este caso, cuando el usuario toma o recibe fotos nuevas, queremos asegurarnos de que usamos las identidades previamente reconocidas para las mismas personas.
1 |
private void saveAlbum(){ |
2 |
byte[] albumBuffer = mFacialProcessing.serializeRecogntionAlbum(); |
3 |
SharedPreferences sharedPreferences = mContext.getSharedPreferences(TAG, 0); |
4 |
SharedPreferences.Editor editor = sharedPreferences.edit(); |
5 |
editor.putString(KEY_IDENTITY_ALBUM, Arrays.toString(albumBuffer)); |
6 |
editor.commit(); |
7 |
}
|
Paso 4: Procesando la imagen
Finalmente estamos listos para desarrollar el método del gancho final y usar la clase FacialProcessing. Los siguientes bloques de código pertenecen al método processImage. Los he separado por claridad.
1 |
private void processImage(ContentValues contentValues, Uri contentUri){ |
2 |
long photoRowId = ContentUris.parseId(contentUri); |
3 |
|
4 |
String uriAsString = contentValues.getAsString(GalleryContract.PhotoEntity.COLUMN_URI); |
5 |
Uri uri = Uri.parse(uriAsString); |
6 |
Bitmap bitmap = null; |
7 |
try{ |
8 |
bitmap = ImageUtils.getImage(mContext, uri); |
9 |
} catch(IOException e){ |
10 |
return; |
11 |
}
|
12 |
|
13 |
if(bitmap != null) { |
14 |
// continued below (1)
|
15 |
}
|
16 |
}
|
El método toma como referencia una instancia de la clase ContentValues, que contiene los metadatos de esta imagen, junto con el URI que apunta a la imagen. Usamos esto para cargar la imagen en la memoria.
El siguiente fragmento de código reemplaza el comentario anterior // continúa debajo (1).
1 |
if( !mFacialProcessing.setBitmap(bitmap)){ |
2 |
return; |
3 |
}
|
4 |
int numFaces = mFacialProcessing.getNumFaces(); |
5 |
|
6 |
if(numFaces > 0){ |
7 |
FaceData[] faceDataArray = mFacialProcessing.getFaceData(); |
8 |
|
9 |
if( faceDataArray == null){ |
10 |
Log.w(TAG, contentUri.toString() + " has been returned a NULL FaceDataArray"); |
11 |
return; |
12 |
}
|
13 |
|
14 |
for(int i=0; i<faceDataArray.length; i++) { |
15 |
FaceData faceData = faceDataArray[i]; |
16 |
if(faceData == null){ |
17 |
continue; |
18 |
}
|
19 |
// continued below (2)
|
20 |
}
|
21 |
}
|
Como se mencionó anteriormente, primero pasamos la imagen estática a la instancia FacialProcessing a través del método setBitmap. El uso de este método utiliza implícitamente el modo FP_MODE_STILL. Este método devuelve True si la imagen se procesó correctamente y False si el proceso falló.
El método alternativo para procesar imágenes de transmisión (típicamente para frames de vista previa de la cámara) es:
1 |
public boolean setFrame(byte[] yuvData, int frameWidth, int frameHeight, boolean isMirrored, FacialProcessing.PREVIEW_ROTATION_ANGLE rotationAngle) |
La mayoría de los parámetros son obvios. Debe pasar si el frame está volteado (esto suele ser necesario para la cámara frontal) y si se ha aplicado alguna rotación (generalmente se configura mediante el método setDisplayOrientation de una instancia de cámara llamada Camera).
A continuación, consultamos el número de caras detectadas y solo continuamos si se encuentra al menos una. El método getFaceData devuelve los detalles de cada cara detectada como una matriz de objetos FaceData, donde cada objeto FaceData encapsula las características faciales que incluyen:
- Límite de cara (
FACE_RECT) - Ubicaciones de cara, boca y ojo (
FACE_COORDINATES) - Contorno de la cara (
FACE_CONTOUR) - Grado de sonrisa (
FACE_SMILE)
- Dirección de los ojos (
FACE_GAZE)
- Indicador que muestra si alguno de los ojos (o ambos ojos) parpadea (
FACE_BLINK)
- Guiñada, inclinación y balanceo de la cara (
FACE_ORIENTATION)
- Identificación generada o derivada (
FACE_IDENTIFICATION)
Hay una sobrecarga en este método que toma un conjunto de enumeraciones (como se describió anteriormente) para incluir puntos de características, eliminando/minimizando los cálculos redundantes.
1 |
public FaceData[] getFaceData(java.util.EnumSet<FacialProcessing.FP_DATA> dataSet) throws java.lang.IllegalArgumentException |
Ahora pasamos a inspeccionar el objeto FaceData para extraer la identidad y las características. Primero veamos cómo se hace el reconocimiento facial.
El siguiente fragmento de código reemplaza el comentario anterior //continúa debajo (2).
1 |
int personId = faceData.getPersonId(); |
2 |
if(personId == FacialProcessingConstants.FP_PERSON_NOT_REGISTERED){ |
3 |
personId = mFacialProcessing.addPerson(i); |
4 |
} else{ |
5 |
if(mFacialProcessing.updatePerson(personId, i) != FacialProcessingConstants.FP_SUCCESS){ |
6 |
// TODO handle error
|
7 |
}
|
8 |
}
|
9 |
|
10 |
long identityRowId = getOrInsertPerson(personId); |
11 |
|
12 |
// continued below (3)
|
Primero solicitamos la identificación de la persona asignada mediante el método getPersonId. Esto devolverá -111 (FP_PERSON_NOT_REGISTERED) si no existe identidad en el álbum cargado actualmente, de lo contrario devolverá el id de una persona correspondiente del álbum cargado.
Si no existe identidad, la agregamos a través del método addPerson del objeto FacialProcessing, pasándole el índice del elemento FaceData que estamos inspeccionando actualmente. El método devuelve la identificación de la persona asignada si tiene éxito, de lo contrario devolverá un error. Esto ocurre cuando intentas agregar una identidad que ya existe.
Alternativamente, cuando la persona fue emparejada con una identidad almacenada en nuestro álbum cargado, llamamos al método updatePerson del objeto FacialProcessing, pasándole el id existente y el índice del ítem FaceData. Agregar a una persona varias veces aumenta el rendimiento del reconocimiento. Puedes agregar hasta diez caras para una sola persona.
La línea final simplemente devuelve el ID de identidad asociado de nuestra base de datos, insertándolo si el ID de persona aún no existe.
No se muestra arriba, pero la instancia de FaceData expone el método getRecognitionConfidence para devolver la confianza de reconocimiento (0 a 100). Dependiendo de tus necesidades, puedes usar esto para influir en el flujo.
El fragmento final muestra cómo consultar cada una de las otras funciones desde la instancia de FaceData. En esta demostración, no hacemos uso de ellos, pero con un poco de imaginación estoy seguro de que puedes pensar en formas de hacer un buen uso de ellos.
El siguiente fragmento de código reemplaza el comentario anterior //continúa debajo (3).
1 |
int smileValue = faceData.getSmileValue(); |
2 |
int leftEyeBlink = faceData.getLeftEyeBlink(); |
3 |
int rightEyeBlink = faceData.getRightEyeBlink(); |
4 |
int roll = faceData.getRoll(); |
5 |
PointF gazePointValue = faceData.getEyeGazePoint(); |
6 |
int pitch = faceData.getPitch(); |
7 |
int yaw = faceData.getYaw(); |
8 |
int horizontalGaze = faceData.getEyeHorizontalGazeAngle(); |
9 |
int verticalGaze = faceData.getEyeVerticalGazeAngle(); |
10 |
Rect faceRect = faceData.rect; |
11 |
|
12 |
insertNewPhotoIdentityRecord(photoRowId, identityRowId, |
13 |
gazePointValue, horizontalGaze, verticalGaze, |
14 |
leftEyeBlink, rightEyeBlink, |
15 |
pitch, yaw, roll, |
16 |
smileValue, faceRect); |
Eso completa el código de procesamiento. Si regresas a la galería y tocas una imagen, deberías verla filtrando las fotos que no contienen personas identificadas en la foto seleccionada.
Conclusión
Comenzamos este tutorial sobre cómo la tecnología se puede utilizar para ayudar a organizar el contenido del usuario. En el contexto de la computación consciente, cuyo objetivo es utilizar el contexto como una clave implícita para enriquecer la interacción empobrecida de los seres humanos a las computadoras, lo que facilita la interacción con las computadoras, esto se conoce como auto-etiquetado. Al marcar el contenido con datos más significativos y útiles, tanto para la computadora como para nosotros, permitimos un filtrado y procesamiento más inteligente.
Hemos visto que esto se usa con frecuencia con contenido de texto, el ejemplo más obvio son los filtros de correo no deseado y, más recientemente, los lectores de noticias, pero no tanto con el contenido multimedia enriquecido, como fotos, música y video. Herramientas como el SDK de Snapdragon nos brindan la oportunidad de extraer características significativas de los medios enriquecidos, exponiendo sus propiedades al usuario y la computadora.
No es difícil imaginar cómo podría ampliar nuestra aplicación para permitir el filtrado basado en el sentimiento mediante el uso de una sonrisa como la principal característica o actividad social contando el número de caras. Se puede ver una implementación de este tipo en esta característica de Smart Gallery.



