Detección de rostros en video: Raspberry Pi y Neural Compute Stick

Hace aproximadamente un año, Intel Movidius lanzó un dispositivo para la inferencia eficiente de redes neuronales convolucionales: Movidius Neural Compute Stick (NCS). Este dispositivo permite el uso de redes neuronales para el reconocimiento o detección de objetos en condiciones de consumo de energía limitado, incluso en tareas de robótica. NCS tiene una interfaz USB y no consume más de 1 vatio. En este artículo, hablaré sobre la experiencia de usar NCS con la Raspberry Pi para la tarea de detectar rostros en video, incluyendo tanto la capacitación del detector Mobilenet-SSD como el lanzamiento en Raspberry.

Todo el código se puede encontrar en mis dos repositorios: capacitación de detectores y demostraciones de detección de rostros .



En mi primer artículo, ya escribí sobre la detección de rostros usando NCS: luego estábamos hablando de un detector YOLOv2 , que convertí del formato Darknet al formato Caffe , y luego lo lancé en NCS. El proceso de conversión resultó no ser trivial: dado que estos dos formatos definen la última capa del detector de diferentes maneras, la salida de la red neuronal tuvo que analizarse por separado, en la CPU, utilizando un código de Darknet. Además, este detector no me satisfizo tanto en velocidad (hasta 5.1 FPS en mi computadora portátil) como en precisión; más tarde me convencí de que debido a su sensibilidad a la calidad de imagen era difícil obtener un buen resultado en Raspberry Pi.

Al final, decidí entrenar mi detector. La elección recayó en el detector SSD con el codificador Mobilenet : la convolución ligera de Mobilenet le permite alcanzar alta velocidad sin pérdida de calidad, y el detector SSD no es inferior a YOLO y funciona en el NCS de fábrica.

Cómo funciona el detector Mobilenet-SSD
Comencemos con Mobilenet. En esta arquitectura esta completa la convolución (en todos los canales) se reemplaza por dos convoluciones ligeras: primero por separado para cada canal y luego completar convolución Después de cada convolución, se usan BatchNorm y Non-linearity (ReLU). La primera convolución de red que recibe una imagen como entrada generalmente se deja completa. Esta arquitectura puede reducir significativamente la complejidad de los cálculos debido a una ligera disminución en la calidad de las predicciones. Hay una opción más avanzada , pero aún no la he probado.

SSD (Single Shot Detector) funciona así: en las salidas de varios codificadores de convolución se cuelgan en dos capa convolucional: uno predice las probabilidades de las clases, el otro, las coordenadas del cuadro delimitador. Hay una tercera capa que proporciona las coordenadas y las posiciones de los cuadros predeterminados en el nivel actual. El significado es: la salida de cualquier capa se divide naturalmente en celdas; más cerca del final de la red neuronal, se están volviendo más pequeños (en este caso, debido a la convolución con stride=2 ), y el campo de visión de cada célula aumenta. Para cada celda en cada una de varias capas seleccionadas, establecemos varios marcos predeterminados de diferentes tamaños y con diferentes relaciones de aspecto, y utilizamos capas convolucionales adicionales para corregir coordenadas y predecir las probabilidades de clase para cada uno de esos marcos. Por lo tanto, un detector SSD (como YOLO) siempre considera el mismo número de cuadros. El mismo objeto se puede detectar en diferentes capas: durante el entrenamiento, la señal se envía a todos los cuadros que se cruzan con el objeto con bastante fuerza, y durante la aplicación, las detecciones se combinan utilizando la supresión no máxima (NMS). La capa final combina las detecciones de todas las capas, considera sus coordenadas completas, corta el umbral de probabilidad y produce NMS.

Entrenamiento de detectores


Arquitectura


El código para entrenar el detector se encuentra aquí .

Decidí usar el detector Mobilenet-SSD listo para usar entrenado en el PASCAL VOC0712 y entrenarlo para detectar rostros. En primer lugar, ayuda a entrenar la red mucho más rápido y, en segundo lugar, no tiene que reinventar la rueda.

El proyecto original incluía el script gen.py , que literalmente recopilaba el archivo de modelo .prototxt , sustituyendo los parámetros de entrada. Lo transferí a mi proyecto, ampliando un poco la funcionalidad. Este script le permite generar cuatro tipos de archivos de configuración:

  • tren : en la entrada - una base de entrenamiento LMDB, en la salida - una capa con el cálculo de la función de pérdida y sus gradientes, hay BatchNorm
  • prueba : en la entrada, la base de prueba LMDB, en la salida, la capa con el cálculo de la calidad (precisión media promedio), hay BatchNorm
  • desplegar : en la entrada - la imagen, en la salida - la capa con predicciones, falta BatchNorm
  • deploy_bn : en la entrada, la imagen, en la salida, la capa con predicciones, hay BatchNorm

Agregué la última opción más tarde, para que en las secuencias de comandos pueda cargar y convertir la cuadrícula de BatchNorm sin tocar la base de datos LMDB; de lo contrario, en ausencia de la base de datos, nada funcionaría. (En general, me parece extraño que en Caffe la fuente de datos esté configurada en la arquitectura de red; esto al menos no es muy práctico).

¿Cómo se ve la arquitectura de red (breve)
  • Iniciar sesión:
  • Full conv conv conv : 32 canales, stride=2
  • Convolución Mobilenet conv1 - conv11 : 64, 128, 128, 256, 256, 512 ... 512 canales, algunos tienen stride=2
  • Capa de detección:
  • Convolución Mobilenet conv12, conv13 : 1024 canales, conv12 tiene stride=2
  • Capa de detección:
  • Convolución completa conv14_1, conv14_2 : 256, 512 canales, el primer kernel_size=1 , el segundo stride=2
  • Capa de detección:
  • Convolución completa conv15_1, conv15_2 : 128, 256 canales, el primer kernel_size=1 , el segundo stride=2
  • Capa de detección:
  • Convoluciones conv16_1, conv16_2 completas: 128, 256 canales, el primer kernel_size=1 , el segundo stride=2
  • Capa de detección:
  • Convolución completa conv17_1, conv17_2 : 64, 128 canales, el primer kernel_size=1 , el segundo stride=2
  • Capa de detección:
  • Salida de detección de capa final


He corregido ligeramente la arquitectura de la red. Lista de cambios:

  • Obviamente, el número de clases ha cambiado a 1 (sin contar el fondo).
  • Limitaciones en la relación de aspecto de los parches de corte durante el entrenamiento: cambiado de en (Decidí simplificar un poco la tarea y no aprender de imágenes demasiado estiradas).
  • De los marcos predeterminados, solo quedaron los cuadrados, dos para cada celda. Reduje considerablemente sus tamaños, ya que las caras son significativamente más pequeñas que los objetos en el clásico problema de detección de objetos.

Caffe calcula los tamaños de fotograma predeterminados de la siguiente manera: tener un tamaño de fotograma mínimo y máximo , crea un marco pequeño y grande con dimensiones y . Como quería detectar rostros lo más pequeños posible, calculé el stride completo para cada capa de detección y equiparé el tamaño mínimo de fotograma. Con estos parámetros, los pequeños marcos predeterminados se ubicarán uno cerca del otro y no se intersecarán. Entonces, al menos tenemos una garantía de que la intersección con el objeto existirá para algún tipo de marco. Establezco el tamaño máximo el doble. Para las capas conv16_2, conv17_2, configuro las dimensiones en el ojo para que sean las mismas. De esta manera para todas las capas fueron:

Cómo se ven algunos marcos predeterminados (ruido para mayor claridad)


Datos


Usé dos conjuntos de datos : WIDER Face y FDDB . WIDER contiene muchas imágenes con caras muy pequeñas y borrosas, y FDDB está más inclinado a imágenes grandes de caras (y un orden de magnitud menor que WIDER). El formato de anotación es ligeramente diferente en ellos, pero estos ya son detalles.

Para el entrenamiento, no utilicé todos los datos: arrojé caras demasiado pequeñas (menos de seis píxeles o menos del 2% del ancho de la imagen), arrojé todas las imágenes con una relación de aspecto de menos de 0.5 o más de 2, arrojé todas las imágenes marcadas como "borrosas" en el conjunto de datos WIDER, dado que correspondían en su mayor parte a personas muy pequeñas, y tuve que alinear al menos de alguna manera la proporción de caras pequeñas y grandes. Después de eso, hice todos los cuadros cuadrados, expandiendo el lado más pequeño: decidí que no estaba muy interesado en las proporciones faciales, y la tarea para la red neuronal se simplificó un poco. También descarté todas las imágenes en blanco y negro, de las cuales había pocas, y en las que el script de compilación de la base de datos falla.

Para usarlos para entrenamiento y pruebas, necesita armar una base LMDB a partir de ellos. Cómo hacerlo:

  • Para cada imagen, el marcado se crea en formato .xml .
  • Se train.txt archivo train.txt con líneas de la forma "path/to/image.png path/to/labels.xml" , lo mismo se crea para la prueba.
  • Se test_name_size.txt archivo test_name_size.txt con líneas de la forma "test_image_name height width"
  • Crea un archivo labelmap.prototxt con coincidencias de labelmap.prototxt numéricas


Se ssd-caffe/scripts/create_annoset.py (ejemplo del Makefile):

 python3 /opt/movidius/ssd-caffe/scripts/create_annoset.py --anno-type=detection \ --label-map-file=$(wider_dir)/labelmap.prototxt --min-dim=0 --max-dim=0 \ --resize-width=0 --resize-height=0 --check-label --encode-type=jpg --encoded \ --redo $(wider_dir) \ $(wider_dir)/trainval.txt $(wider_dir)/WIDER_train/lmdb/wider_train_lmdb ./data 

labelmap.prototxt
 item { name: "none_of_the_above" label: 0 display_name: "background" } item { name: "face" label: 1 display_name: "face" } 



Ejemplo de marcado .xml
 <?xml version="1.0" ?> <annotation> <size> <width>348</width> <height>450</height> <depth>3</depth> </size> <object> <name>face</name> <bndbox> <xmin>161</xmin> <ymin>43</ymin> <xmax>241</xmax> <ymax>123</ymax> </bndbox> </object> </annotation> 


El uso de dos conjuntos de datos al mismo tiempo solo significa que debe fusionar cuidadosamente los archivos correspondientes en pares, sin olvidar registrar correctamente las rutas, así como barajar el archivo para el entrenamiento.

Después de eso, puedes comenzar a entrenar.

Entrenamiento


El código para la capacitación modelo se puede encontrar en mi Colab Notebook .

Realicé la capacitación en Google Colaboratory, porque mi computadora portátil apenas realizó las pruebas de la red y, en general, colgó la capacitación. La colaboración me permitió entrenar la red lo suficientemente rápido y gratis. El único inconveniente es que tuve que escribir un script de compilación SSD-Caffe para el Colaboratorio (que incluye cosas tan extrañas como la recompilación de impulso y la edición de la fuente), que toma alrededor de 40 minutos. Se pueden encontrar más detalles en mi publicación anterior .

El Colaboratorio tiene una característica más: después de 12 horas, el automóvil muere, borrando permanentemente todos los datos. La mejor manera de evitar la pérdida de datos es montar su disco de Google en el sistema y ahorrar pesos de red allí cada 500-1000 iteraciones de entrenamiento.

En cuanto a mi detector, en una sesión en Colaboratory logró desaprender 4.500 iteraciones, y se entrenó por completo en dos sesiones.

La calidad de las predicciones (precisión promedio promedio) en el conjunto de datos de prueba que destaqué (fusionó WIDER y FDDB con las restricciones enumeradas anteriormente) fue de aproximadamente 0.87 para el mejor modelo. Para medir mAP en las escalas guardadas durante el entrenamiento, hay un script scripts/plot_map.py .

El detector funciona en un ejemplo (muy extraño) de un conjunto de datos:


Lanzamiento en NCS


Una demostración de detección de rostros está aquí .

Para compilar una red neuronal para el Neural Compute Stick, necesita Movidius NCSDK : contiene utilidades para compilar y perfilar redes neuronales, así como las API de C ++ y Python. Vale la pena señalar que la segunda versión fue lanzada recientemente, que no es compatible con la primera: todas las funciones de API fueron renombradas por alguna razón, el formato interno de las redes neuronales cambió, FIFO fue agregado para interactuar con NCS y (finalmente) la conversión automática de flotante de 32 bits a float 16 bit, que faltaba tanto en C ++. Actualicé todos mis proyectos a la segunda versión, pero dejé un par de muletas para compatibilidad con la primera.

Después de entrenar el detector, vale la pena fusionar capas BatchNorm con convoluciones adyacentes para acelerar la red neuronal. El script merge_bn.py esto desde aquí , que también tomé prestado del proyecto Mobilenet-SSD.

Luego debe llamar a la utilidad mvNCCompile , por ejemplo:

 mvNCCompile -s 12 -o graph_ssd -w ssd-face.caffemodel ssd-face.prototxt 

Hay un objetivo graph_ssd para esto en el Makefile del proyecto. El archivo graph_ssd resultante es una descripción de red neuronal en un formato comprendido por NCS.

Ahora sobre cómo interactuar con el dispositivo en sí. El proceso no es muy complicado, pero requiere una cantidad bastante grande de código. La secuencia de acciones es aproximadamente la siguiente:

  • Obtenga el descriptor del dispositivo por número de serie
  • Dispositivo abierto
  • Lea el archivo de red neuronal compilado en el búfer (como un archivo binario)
  • Crear un gráfico de cálculo vacío para NCS
  • Coloque el gráfico en el dispositivo usando los datos del archivo y seleccione FIFO para él en la entrada / salida; ahora se puede liberar el búfer con el contenido del archivo
  • Detector de inicio:
    • Obtenga la imagen de la cámara (o de cualquier otra fuente)
    • Procesarlo: escalar al tamaño deseado, convertir a float32 y lanzar al rango [-1,1]
    • Subir imagen al dispositivo y solicitar inferencia
    • Solicitar un resultado (el programa se bloqueará hasta que se reciba el resultado)
    • Analiza el resultado, selecciona los cuadros de los objetos (sobre el formato - más adelante)
    • Mostrar predicciones

  • Libere todos los recursos: elimine FIFO y el gráfico de cálculo, cierre el dispositivo y quite su asa

Casi todas las acciones con NCS tienen su propia función separada, y en C ++ se ve muy engorroso, y debe monitorear cuidadosamente la liberación de todos los recursos. Para no sobrecargar el código, creé una clase de contenedor para trabajar con NCS . En él, todo el trabajo de inicialización se oculta en el constructor y la función load_file , y en la liberación de recursos, en el destructor, y trabajar con NCS se reduce a llamar a métodos de clase 2-3. Además, hay una función conveniente para explicar los errores que han ocurrido.

Cree un contenedor pasando el tamaño de entrada y el tamaño de salida (número de elementos) al constructor:

 NCSWrapper NCS(NETWORK_INPUT_SIZE*NETWORK_INPUT_SIZE*3, NETWORK_OUTPUT_SIZE); 

Cargamos el archivo compilado con la red neuronal, inicializando simultáneamente todo lo que necesitamos:

 if (!NCS.load_file("./models/face/graph_ssd")) { NCS.print_error_code(); return 0; } 

Convertimos la imagen a float32 (la image es cv::Mat en el formato CV_32FC3 ) y la descargamos al dispositivo:

 if(!NCS.load_tensor_nowait((float*)image.data)) { NCS.print_error_code(); break; } 

Obtenemos el resultado (el result es un puntero de float libre, el contenedor de resultados es compatible con el contenedor); hasta el final de los cálculos, el programa está bloqueado:

 if(!NCS.get_result(result)) { NCS.print_error_code(); break; } 

De hecho, el contenedor también tiene un método que le permite cargar datos y obtener el resultado al mismo tiempo: load_tensor((float*)image.data, result) . Me negué a usarlo por una razón: usando métodos separados, puede acelerar ligeramente la ejecución del código. Después de cargar la imagen, la CPU permanecerá inactiva hasta que llegue el resultado de la ejecución con NCS (en este caso es de aproximadamente 100 ms), y en este momento puede hacer un trabajo útil: leer un nuevo marco y convertirlo, así como mostrar detecciones anteriores . Así es exactamente como se implementa el programa de demostración, en mi caso aumenta ligeramente el FPS. Puede ir más allá e iniciar el procesamiento de imágenes y el detector de caras de forma asincrónica en dos flujos diferentes; esto realmente funciona y le permite acelerar un poco más, pero no está implementado en el programa de demostración.

Como resultado, el detector devuelve una matriz flotante de tamaño 7*(keep_top_k+1) . Aquí, keep_top_k es el parámetro especificado en el archivo .prototxt del modelo y muestra cuántas detecciones (en el orden de disminución de la confianza) deben devolverse. Este parámetro, así como el parámetro responsable de filtrar las detecciones por el valor de confianza mínimo, y los parámetros de supresión no máxima se pueden configurar en el archivo .prototxt del modelo en la última capa. Vale la pena señalar que si Caffe devuelve tantas detecciones como se encontraron en la imagen, entonces NCS siempre devuelve keep_top_k detecciones para que el tamaño de la matriz sea constante.

El conjunto de resultados en sí está keep_top_k+1 siguiente manera: si lo consideramos como una matriz con keep_top_k+1 filas y 7 columnas, entonces en la primera fila, en el primer elemento habrá el número de detecciones, y a partir de la segunda fila habrá las detecciones en el formato "garbage, class_index, class_probability, x_min, y_min, x_max, y_max" . Las coordenadas se especifican en el rango [0,1], por lo que deberán multiplicarse por el alto / ancho de la imagen. Los elementos restantes de la matriz serán basura. En este caso, la supresión no máxima se realiza automáticamente, incluso antes de que se obtenga el resultado (parece, justo en el NCS).

Detector de análisis
 void get_detection_boxes(float* predictions, int w, int h, float thresh, std::vector<float>& probs, std::vector<cv::Rect>& boxes) { int num = predictions[0]; float score = 0; float cls = 0; for (int i=1; i<num+1; i++) { score = predictions[i*7+2]; cls = predictions[i*7+1]; if (score>thresh && cls<=1) { probs.push_back(score); boxes.push_back(Rect(predictions[i*7+3]*w, predictions[i*7+4]*h, (predictions[i*7+5]-predictions[i*7+3])*w, (predictions[i*7+6]-predictions[i*7+4])*h)); } } } 


Raspberry Pi Características de lanzamiento


El programa de demostración en sí se puede ejecutar en una computadora o computadora portátil normal con Ubuntu, o en una Raspberry Pi con un Raspbian Stretch. Estoy usando un Raspberry Pi 2 modelo B, pero la demostración también debería funcionar en otros modelos. El archivo MAKE del proyecto contiene dos objetivos para cambiar de modo: make switch_desk para la computadora / laptop y make switch_rpi para la Raspberry Pi. La diferencia fundamental en el código del programa es que, en el primer caso, OpenCV se usa para leer datos de la cámara y, en el segundo caso, la biblioteca RaspiCam . Para ejecutar la demostración en Raspberry, debe compilarla e instalarla.

Ahora un punto muy importante: instalar NCSDK. Si sigue las instrucciones de instalación estándar en la Raspberry Pi, no terminará en nada bueno: el instalador intentará arrastrar y compilar SSD-Caffe y Tensorflow. En cambio, el NCSDK debe compilarse en modo solo API . En este modo, solo estarán disponibles las API de C ++ y Python (es decir, no será posible compilar y perfilar gráficos de redes neuronales). Esto significa que el gráfico de la red neuronal primero debe compilarse en una computadora normal y luego copiarse a Raspberry. Por conveniencia, agregué dos archivos compilados al repositorio, para YOLO y para SSD.

Otro punto interesante es la conexión puramente física de NCS a Raspberry. Parece que no es difícil conectarlo a un conector USB, pero debe recordar que su carcasa bloqueará los otros tres conectores (es bastante saludable, ya que actúa como un radiador). La salida más fácil es conectarlo a través de un cable USB.

También vale la pena tener en cuenta que la velocidad de ejecución variará para diferentes versiones de USB (para esta red neuronal particular: 102 ms para USB 3.0, 92 ms para USB 2.0).

Ahora sobre el poder de la NCS. Según la documentación, consume hasta 1 vatio (a 5 voltios en el conector USB será de hasta 200 ma; en comparación: la cámara Raspberry consume hasta 250 ma). Cuando funciona con un cargador regular de 5 voltios, 2 amperios, todo funciona muy bien. Sin embargo, intentar conectar dos o más NCS a Raspberry puede causar problemas. En este caso, se recomienda utilizar un divisor USB con posibilidad de alimentación externa.

En Raspberry, la demostración es más lenta que en una computadora / laptop: 7.2 FPS versus 10.4 FPS. Esto se debe a varios factores: en primer lugar, es imposible deshacerse de los cálculos en la CPU, pero se realizan mucho más lentamente; en segundo lugar, la velocidad de transferencia de datos afecta (para USB 2.0).

Además, para comparar, intenté ejecutar un detector de rostros en Raspberry YOLOv2 desde mi primer artículo, pero funcionó muy mal: a una velocidad de 3.6 FPS, pierde muchas caras incluso en cuadros simples. Aparentemente, es muy sensible a los parámetros de la imagen de entrada, cuya calidad en el caso de la cámara Raspberry está lejos de ser ideal. SSD funciona mucho más estable, aunque tuve que ajustar un poco la configuración de video en la configuración de RapiCam. a veces también extraña las caras en el marco, pero rara vez lo hace. Para aumentar la estabilidad en aplicaciones reales, puede agregar un rastreador centroide simple.

Por cierto: lo mismo se puede reproducir en Python, hay un tutorial sobre PyImageSearch (Mobilenet-SSD se usa para la tarea de detección de objetos).

Otras ideas


También probé un par de ideas para acelerar la red neuronal en sí:

Primera idea: puede dejar solo la detección de las capas conv11 y conv13 , y eliminar todas las capas adicionales. Obtendrá un detector que detecta solo caras pequeñas y funciona un poco más rápido. En general, no vale la pena.

La segunda idea fue interesante, pero no funcionó: intenté lanzar convoluciones desde la red neuronal con pesos cercanos a cero, con la esperanza de que fuera más rápido. Sin embargo, hubo pocas convoluciones de este tipo, y su eliminación solo ralentizó ligeramente la red neuronal (el único presentimiento: esto se debe al hecho de que el número de canales ha dejado de ser una potencia de dos).

Conclusión


Pensé en detectar rostros en Raspberry durante mucho tiempo, como una subtarea de mi proyecto robótico. No me gustaron los detectores clásicos en términos de velocidad y calidad, y decidí probar métodos de redes neuronales, al mismo tiempo que probaba el Neural Compute Stick, como resultado de lo cual había dos proyectos en GitHub y tres artículos sobre Habré (incluido el actual). En general, el resultado me conviene, lo más probable es que use este detector en mi robot (tal vez haya otro artículo al respecto). Vale la pena señalar que mi solución puede no ser óptima; sin embargo, este es un proyecto de capacitación, hecho en parte por curiosidad por el NCS. Sin embargo, espero que este artículo sea útil para alguien.

Source: https://habr.com/ru/post/es424973/


All Articles