AuraView – Espejo inteligente

Grupo 1

Adrián García Martín

Sergio Reguero Elez

Fernando Silvestre Menéndez

Tabla de contenido

  • Introducción
  • Estructura e implementación
    • Pantalla y cámara
    • Iluminación
    • Sonido
    • Circuito unido
  • Materiales y presupuesto
    • Material dado en el aula
    • Material extra
  • Código
  • Reparto de tareas
  • Problemas encontrados
    • Problema de correlación de valores RGB
    • Problema con funcionamiento del sensor
    • Problema al unir código
  • Fases del funcionamiento de AuraView
    • Pantalla y cámara
    • Iluminación
    • Sonido
  • Vídeo enseñando el funcionamiento

Introducción

En esta entrega se nos plantea la idea de poder crear un proyecto bien presentado, original y complicado de hacer mediante hardware y software con Arduino visto en clase.

A través de materiales dados por los profesores y otros comprados por nuestra propia cuenta, hemos creado nuestro espejo inteligente AuraView, que puede reflejar y a la vez ser interactivo para el usuario con funciones útiles como mostrar las noticias, el tiempo en la zona, la hora y un calendario en el que se puedan ver los próximos eventos de cada usuario con una pantalla detrás del reflejo.

El espejo podrá reconocer al usuario que esté utilizando el espejo para mostrar la pantalla que este mismo habrá personalizado a su gusto. Cada usuario puede tener una pantalla diferente.

Estructura e implementación

La realización del espejo inteligente se hizo en tres partes diferenciadas, cada una de ellas con su circuito hardware, su programación y su finalidad. Dichas partes son la pantalla y cámara (que van unidas ya que su funcionamiento está relacionado), la iluminación y el sonido. Explicamos primero las partes por separado y luego veremos un circuito completo de todas ellas unidas.

Pantalla y cámara

Circuito y programación

Para la pantalla, contábamos con un monitor, el cual serviría de base y soporte para todo el proyecto, su función era proyectar la imagen proveniente de la Raspberry Pi 3B, y así mostrar las distintas pantallas, en base a la cara que se reconociera en ese momento. Encima de su pantalla, se le colocó un plástico acrílico, al cual se le puso un vinilo reflejante el cual permitía pasar luz desde detrás, mientras que a su vez reflejaba la luz proveniente de frente, logrando así ese efecto de espejo. Para lograr el soporte de esa parte reflectante, se cortaron dos piezas de metal y así hacían de soporte del marco.

A su vez, para mantener el proyecto lo más compacto y portátil posible, se ancló una caja de cartón a la parte de atrás del monitor, logrando sujetarla con los tornillos del propio monitor, logrando una gran firmeza. En esa caja se guardaron todos los componentes necesarios.

Para la cámara encargada del reconocimiento facial, se hizo uso de una ESP32-CAM. Con un sensor OV2640, el cual es el compatible para el formato de imagen que se deseaba capturar.

Para el reconocimiento facial, se hizo gran uso de un programa de ejemplo que se proporciona en el Arduino IDE, llamado ‘CameraWebServer’, el cual ya incluye el código necesario para el reconocimiento facial y la diferenciación entre personas. Sin embargo, ese código tal cual no era útil, ya que el almacenamiento de las caras registradas era volátil, por tanto, al apagar la placa, se borraban los rostros identificados, para ello se tuvo que incluir unas líneas para cargar en la propia memoria de la placa esas caras registradas.

El código anterior, era necesario para registrar las caras en el fichero persistente, pero para el código final, se optó por deshacerse del código encargado del servidor web, ya que la limitada RAM de la ESP32 causaba errores, y era un uso de recursos innecesarios, por tanto, se introdujo el código de reconocimiento facial y el resto de las funciones fueron suprimidas, logrando así un archivo más ligero y con una consumición menor de recursos.

Para la carga de código se hacía uso de un adaptador el cual permitía mediante un cable micro-usb, la carga de programas a la placa.

Sin embargo, para la versión final, solo era necesarios los pines de alimentación y el TX, ya que se pasaba de hacer la conexión entre la placa y el ordenador, a hacerse una conexión entre la ESP32-CAM y el Arduino UNO R3. El Arduino Uno R3, alimentaba la ESP32-CAM mediante los 5V, y a su vez recibía por el puerto RX, la información transmitida por la cámara.

Cuando la cámara detectaba una cara, mandaba por el puerto serial, el siguiente mensaje “ID: X”, ‘X’ haciendo referencia al número asociado a la cara detectada, o en caso de no reconocer la cara “NO ID”. La placa Uno R3, en base a lo recibido, realizaba las diferentes acciones, como el parpadeo de las luces de colores según si era una cara reconocida o no, y a su vez reenviaba por su puerto serial, el mensaje recibido, para que la Raspberry leyese la id, y en consecuencia cargar la pantalla correspondiente.

En la Raspberry, se puso un código de tipo Shell, que se ejecutaba automáticamente al encenderse, el cual cargaba la pantalla inicial, un HTML, y después se ponía a escuchar por un puerto USB, para cargar las distintas pantallas.

Para unas plantillas HTML, con un acabado profesional, se hizo uso del sitio web www.dakboard.com, el cual permitía la creación de pantallas HTML personalizadas a gran nivel, y con un acabado más profesional. Abajo hay un ejemplo de ello.

Iluminación

Circuito

La corriente sale a través del pin de 5 voltios de la placa de Arduino a través de un cable rojo. Este cable está conectado al rail positivo de la protoboard. En este carril está conectado otro cable el cual va conectado al interruptor (que nos servirá para dar corriente o no a la tira led) en uno de los contactos de este. Desde el otro contacto del interruptor se conecta directamente a la tira led al pin de 5 voltios.

Para controlar los valores RGB utilizamos 3 pines se salida analógica, en el caso de la imagen de arriba utilizamos los pines 11,10 y 6, siendo el pin 11 el correspondiente con el valor de azul, el 10 con el valor de rojo y el 6 con el valor de verde. Desde cada uno de estos pines sale un cable que conecta con los pines correspondientes en la tira led.

Por último, hay otro pequeño circuito que se encarga de cambiar los colores de la tira led. La corriente pasa al botón a través del cable blanco, el cual a su vez está conectado al carril positivo de la protoboard. En caso de que el botón sea presionado la corriente lo atraviesa en diagonal, por lo que es donde conectamos un cable el cual iría hacia el pin digital 4, el cual usaremos como input digital. En caso de que el botón no esté presionado la corriente hace forma de “U”. Sabiendo esto conectamos en esta posición una resistencia que irá hacia el carril negativo de la protoboard. Además, de este carril hay otro cable conectado al pin de tierra de la placa de Arduino.

Programación

Primero definimos los pines para tener registrados en todo momento el pin al que está conectado cada componente con los que vamos a interactuar.

También, definimos una función que se encargará de definir los valores correspondientes de cada uno de los colores, invirtiendo los valores introducidos como parámetros. Esto último se hace debido a que nuestra tira led es de ánodo común en vez de cátodo común por lo que el valor 255 de un color en nuestra tira led correspondería con que ese color esté apagado, lo cual no es lo que buscamos cuando ponemos 255 en el valor de un color. Con una simple resta de 255 menos el valor de dicho color conseguimos que el valor que buscamos en la tira RGB sea el reflejado en esta.

Seguidamente, definimos las variables que vamos a utilizar para leer el botón y cambiar de color. Las variables “estadoBotónActual” y “estadoBotónAnterior” nos servirá para evitar rebotes en el botón y que este no pueda activarse una segunda vez sin que sea la acción que se quiera hacer.

La variable “colores” es un array de arrays. En cada una de las posiciones del array principal se introducen arrays con tres valores. Cada posición del array se corresponde con un color distinto definido por el valor de rojo, azul y verde que es necesario para cada color definido cada uno en una posición del array anidado.

Por último, la variable “colorActual” nos servirá para saber en que color (posición del array colores) nos encontramos. En este caso como hemos optado por que el color con el que se inicie sea siempre el azul hemos inicializado dicha variable con el posición en la que se encuentra este color en el array de colores.

Dentro de la función “setup” llamamos a la función “establecerColor” para inicializar el valor de la tira led al de la variable color actual, que como se ha indicado anteriormente correspondería con el azul.

En la función “loop” empezaremos leyendo el valor del estado actual del botón, es decir, comprobaremos si el botón ha sido pulsado o no. Seguidamente, comprobaremos si estamos en el caso en el que ahora el botón no esté pulsado pero el anterior estado fue que este lo estaba (lo hacemos así para evitar rebotes). En caso de que esta condición se cumpla, actualizaremos el valor de color actual sumándole uno y haciendo el módulo con el número de colores que hemos definido (en este caso es 7). A continuación, llamamos a la función “establecerColor” para definir en cada color su correspondiente valor. Como último detalle a añadir dentro del cuerpo del “if” hemos añadido un pequeño delay para estabilizar. Fuera del condicional actualizamos el valor del botón anterior al actual, debido a que ya hemos realizado todas las operaciones y comprobaciones relacionadas con dicho botón y hasta la siguiente ejecución de loop no se volverá a usar; fase en la que se volverá a leer el estado del botón.

Sonido

Circuito

El sonido lo hemos podido recrear mediante el siguiente circuito hardware:

Los cables rojos corresponden con la corriente positiva (5V), mientras que los cables azules corresponden con la conexión a tierra (GND) y el resto de los colores a los pines de la placa Arduino (el naranja con el pin 8 para la salida del audio de la programación, el morado con el pin 12 para poder parar y seguir la música y el amarillo con el pin 13 para poder cambiar a la siguiente canción).

En primer lugar, tenemos el altavoz que es el componente negro en la parte inferior de la imagen. Este es el encargado de reproducir el sonido que se produce en la programación de Arduino a través de los tonos predeterminados. Va conectado en la parte positiva a tierra (GND) y la parte negativa conectada al potenciómetro que regulará el volumen. Más adelante se detallará la programación del audio.

En cuanto al potenciómetro, este se encarga de regular el volumen proveniente de los tonos de Arduino. Va conectado a tierra (GND) y a la corriente (5V) en los dos extremos como se indica en la imagen (suponemos que en Tinkercad el potenciómetro mira hacia delante). También va conectado al pin 8 ya que antes de que se escuche en el altavoz, va conectado al potenciómetro como regulador de volumen antes de que pase el sonido a dicho altavoz. Este componente no requiere de programación.

Por último, tenemos los botones con los que podemos hacer dos acciones:

  • Parar / Reproducir la música: es el botón de la izquierda de la imagen y sirve para poder parar o reproducir de nuevo las canciones. Va conectado al pin 12 (para que la programación funcione al pulsarlo), a tierra (GND) y la corriente (5V).
  • Pasar a la siguiente canción: es el botón de la derecha de la imagen y sirve para poder cambiar a la siguiente canción. Va conectado al pin 13 (para que la programación funcione al pulsarlo), a tierra (GND) y la corriente (5V).

Ambos botones en una parte están conectados por resistencias de 1KΩ cada una para asegurar que los botones no dejen de funcionar. Tienen implementados una programación como ya hemos dicho antes que la vamos a ver a continuación.

Programación

Hablando sobre la programación, vamos a irla explicando por partes su funcionamiento básico:

El módulo melodias.h corresponde con las canciones almacenadas en Arduino para que se reproduzcan en el código principal de nuestro espejo inteligente. Estas canciones se han podido crear gracias a que Arduino tiene definidos tonos predeterminados que suenan más o menos agudos, lo que hace que se puedan representar canciones con ciertas limitaciones. Como se puede ver en el módulo, primero definimos los tonos que vamos a utilizar y después se establecen las matrices de tonos que representan las canciones, de 100×3 en las que hay 100 tonos con los 3 parámetros que va a necesitar la instrucción tone() vista en clase:

  • Primer parámetro: tono predeterminado de Arduino.
  • Segundo parámetro: frecuencia.
  • Tercer parámetro: duración.

Estas canciones pueden alargarse o acortarse añadiendo o quitando líneas y agregando o quitando espacio en el array (cambiando el número 100). La limitación es que Arduino tiene espacio limitado y se podría agregar solo un cierto número de canciones puesto a que los arrays ocupan demasiado.

Las canciones se han podido crear gracias a una aplicación web que transforma los ficheros .MIDI en código Arduino para representar la canción original. Dicha aplicación web es https://arduinomidi.netlify.app/

Por otro lado, la programación del sonido en el código principal del espejo inteligente tiene varias áreas. La definición e inicialización de los pines necesarios (8, 12 y 13) se hace como el resto teniendo en cuenta que el pin 8 es de salida por la música de salida al altavoz y los otros dos de entrada porque son los botones que vamos a pulsar y que van a afectar al comportamiento del sonido en la programación.

También hay que explicar estas dos variables:

Cada una representa el tamaño de una melodía y se calcula entre el tamaño total de la matriz vista antes y el primer array de 3 elementos para saber el número de iteraciones que tiene que hacer el bucle al reproducir la canción (dicho de otro modo, la duración de la canción).

La variable reproducir determina si la música tiene que reproducirse o no. Se inicializa al principio a false, pero cuando pulsamos al botón de reproducir o pausar (pin 12), esta se convierte en el valor contrario (true) haciendo que la música se reproduzca gracias a la siguiente función que detecta si el botón está pulsado o no y en caso de que si, cambia el valor de reproducir:

Si se volviese a dar al botón otra vez, el valor true de reproducir pasaría a false y dejaría de sonar el altavoz.

Si hemos pulsado el botón para que se reproduzcan las canciones, el siguiente código se ejecuta completamente:

Por tanto, se ejecuta la función reproducirMelodia():

La imagen es un extracto de la función entera. En esta función también se mira si se le da al botón de reproducir o pausar (pin 12) en ciertas partes de la función para que al darle al botón se sepa que se quiere pausar la música. Lo mismo sucede con el botón de cambiar a la siguiente canción (pin 13) que tiene el mismo funcionamiento que el otro botón puesto a que la función pasarSiguienteCancion() también emplea la variable siguiente que indica si se pasa a la siguiente canción o no (y estará inicializada siempre en false para no pasar todo el rato las canciones). El valor de siguiente cambiará a true si se pulsa al botón y cuando la programación entre en la condición, volverá a cambiarlo a false para no cambiar constantemente las canciones. Aquí se muestra la función pasarSiguienteCancion():

En cuanto al sonido de las canciones, se reproduce mediante la función tone() vista en clase y cuando se quiere parar dichas canciones se hace mediante la función notone() vista también en clase.

Circuito unido

A continuación, mostramos todas las partes unidas en un mismo circuito:

El código unido se muestra más adelante en el apartado Código.

Materiales y presupuesto

Material dado en el aula

  • Placa Arduino UNO R3: para poder crear el circuito conectando el hardware a los pines y unirlo con la programación en Arduino desde un ordenador.
  • Placa Protoboard: para poder conectar fuertemente los diversos componentes del circuito y establecer una base.
  • Cables: para poder conectar cada uno de los componentes con sus partes correspondientes.
  • Botones: existen 3 botones:
    • Botón de cambio de color de la iluminación: para que la tira led pueda tener tonos distintos de colores cuando pulsamos el botón.
    • Botón de reproducción / pausa de audio: para que se reproduzca o se pare la música pulsando al botón.
    • Botón de siguiente canción: para que se reproduzca la siguiente canción de la lista establecida en el código. La música tiene que estar sonando para que funcione.
  • Potenciómetro lineal: para poder ajustar el volumen de la música.
  • Resistencias: para asegurar el funcionamiento del circuito y que ningún componente tenga problemas.

Material extra

  • ESP32-CAM: para poder detectar los rostros de las personas e identificar si están registrados o no. En caso de que estén registrados la pantalla mostrará la interfaz personalizada del usuario.
  • Raspberry pi b3: para poder establecer una interfaz de usuario personalizada mediante el monitor.
  • Tira led: para la detección de usuarios registrados o no en el espejo. Si la cámara detecta un usuario registrado, la tira led se encenderá de color verde. En caso contrario, se encenderá de color rojo. También la tira led sirve para iluminar al usuario y mayor personalización.
  • Altavoz: para la reproducción de la música.
  • LG FLATRON L1952S (monitor): es el monitor detrás de la lámina reflectante transparente que muestra la interfaz del usuario establecida en la Raspberry anteriormente mencionada.
  • Lámina reflectante transparente: para poder reflejar al usuario en el espejo y que se pueda ver la interfaz por detrás de dicha lámina.
  • Interruptor: para poder apagar o encender la tira led al gusto del usuario.

Código

En este apartado mostramos el código final uniendo todas las partes. Hemos dividido el código en dos partes. Por un lado, el código funcional y por otro el de la base de datos local de las canciones que se escuchan por el altavoz:

Código funcional

#include "melodias.h"

// Pines LED RGB y Botón
const int pinRojo = 10;
const int pinVerde = 6;
const int pinAzul = 11;
const int pinBotonColor = 4;

// Pines para música
const uint8_t PIN_BOTON_PLAY = 12; // Botón rojo
const uint8_t PIN_BOTON_SIG = 13;  // Botón amarillo
const uint8_t PIN_MELODIA = 8;

// Variables de LED
int estadoBotonActual = 0;
int estadoBotonAnterior = 0;
int colorActual = 2;

String mensajeSerial = "";

const int colores[][3] = {
  {255, 0, 0},    // Rojo
  {0, 255, 0},    // Verde
  {0, 0, 255},    // Azul
  {255, 255, 0},  // Amarillo
  {0, 255, 255},  // Cian
  {255, 0, 255},  // Magenta
  {255, 255, 255} // Blanco
};
const int numColores = 7;

// Variables de música
bool siguiente = false;
bool reproducir = false;
bool ultimoEstadoPlay = LOW;
size_t ultimaNota = 0;
int melodiaActual = 1;
size_t tamanyoMelodia1;
size_t tamanyoMelodia2;

void setup() {
  // Setup de LED RGB
  pinMode(pinRojo, OUTPUT);
  pinMode(pinVerde, OUTPUT);
  pinMode(pinAzul, OUTPUT);
  pinMode(pinBotonColor, INPUT);

  establecerColor(colores[colorActual][0], colores[colorActual][1], colores[colorActual][2]);

  // Setup de música
  pinMode(PIN_BOTON_PLAY, INPUT);
  pinMode(PIN_BOTON_SIG, INPUT);
  pinMode(PIN_MELODIA, OUTPUT);

  tamanyoMelodia1 = (sizeof(MELODIA1) / sizeof(MELODIA1[0]));
  tamanyoMelodia2 = (sizeof(MELODIA2) / sizeof(MELODIA2[0]));

  // Serial
  Serial.begin(115200);
}

void loop() {
  // ================= LED RGB =================
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\n') {
      mensajeSerial.trim();
      Serial.println(mensajeSerial);
      if (mensajeSerial.startsWith("ID: ")) {
        parpadearColor(0, 255, 0, 2); // Verde
      } else if (mensajeSerial.indexOf("NO ID") >= 0) {
        parpadearColor(255, 0, 0, 2); // Rojo
      }
      mensajeSerial = "";
    } else {
      mensajeSerial += c;
    }
  }

  estadoBotonActual = digitalRead(pinBotonColor);
  if (estadoBotonActual == LOW && estadoBotonAnterior == HIGH) {
    Serial.println("Boton Color pulsado");
    colorActual = (colorActual + 1) % numColores;
    establecerColor(colores[colorActual][0], colores[colorActual][1], colores[colorActual][2]);
    delay(50);
  }
  estadoBotonAnterior = estadoBotonActual;

  // ================= Música =================
  actualizarEstadoPlay();

  if (reproducir) {
    if (melodiaActual == 1) {
      reproducirMelodia(MELODIA1, tamanyoMelodia1, 1);
    } else if (melodiaActual == 2) {
      reproducirMelodia(MELODIA2, tamanyoMelodia2, 2);
    }
  }

  delay(10); // Pequeño respiro
}

// ================= FUNCIONES =================

void establecerColor(int rojo, int verde, int azul) {
  analogWrite(pinRojo, 255 - rojo);
  analogWrite(pinVerde, 255 - verde);
  analogWrite(pinAzul, 255 - azul);
}

void parpadearColor(int rojo, int verde, int azul, int repeticiones) {
  int anteriorR = colores[colorActual][0];
  int anteriorG = colores[colorActual][1];
  int anteriorB = colores[colorActual][2];

  for (int i = 0; i < repeticiones; i++) {
    establecerColor(rojo, verde, azul);
    delay(300);
    establecerColor(0, 0, 0);
    delay(300);
  }

  establecerColor(anteriorR, anteriorG, anteriorB);
}

void actualizarEstadoPlay() {
  bool estadoActualPlay = digitalRead(PIN_BOTON_PLAY);
  if (estadoActualPlay == HIGH && ultimoEstadoPlay == LOW) {
    delay(50);
    if (digitalRead(PIN_BOTON_PLAY) == HIGH) {
      reproducir = !reproducir;
      if (!reproducir) {
        noTone(PIN_MELODIA);
      }
      delay(50);
    }
  }
  ultimoEstadoPlay = estadoActualPlay;
}

void pasarSiguienteCancion() {
  if (digitalRead(PIN_BOTON_SIG) == HIGH) {
    delay(50);
    if (digitalRead(PIN_BOTON_SIG) == HIGH) {
      siguiente = true;
    }
  }
}

void reproducirMelodia(const uint16_t NOTAS[][3], size_t duracion, int numeroMelodia) {
  for (size_t i = ultimaNota; i < duracion; i++) {
    actualizarEstadoPlay();
    pasarSiguienteCancion();

    if (siguiente) {
      siguiente = false;
      ultimaNota = 0;
      melodiaActual = (melodiaActual == 1) ? 2 : 1;
      return;
    }

    if (!reproducir) {
      noTone(PIN_MELODIA);
      ultimaNota = i;
      melodiaActual = numeroMelodia;
      return;
    }

    tone(PIN_MELODIA, NOTAS[i][0]);
    delay(NOTAS[i][1]);

    actualizarEstadoPlay();
    if (!reproducir) {
      noTone(PIN_MELODIA);
      ultimaNota = i;
      melodiaActual = numeroMelodia;
      return;
    }

    noTone(PIN_MELODIA);
    delay(NOTAS[i][2]);

    actualizarEstadoPlay();
    if (!reproducir) {
      ultimaNota = i;
      melodiaActual = numeroMelodia;
      return;
    }

    ultimaNota = i + 1;
  }

  ultimaNota = 0;
  melodiaActual = (numeroMelodia == 1) ? 2 : 1;
}

Código de la cámara

#define CAMERA_MODEL_AI_THINKER
#define FORMAT_SPIFFS_IF_FAILED true
#define FACE_ID_PATH "/faceid.dat"

#include "esp_camera.h"
#include <WiFi.h>
#include "camera_pins.h"
#include "fd_forward.h"
#include "fr_forward.h"
#include "SPIFFS.h"
#include "FS.h"

#define ENROLL_CONFIRM_TIMES 5
#define FACE_ID_SAVE_NUMBER 7

static mtmn_config_t mtmn_config = {0};
static int8_t detection_enabled = 0;
static int8_t recognition_enabled = 0;
static int8_t is_enrolling = 0;
static face_id_list id_list = {0};
int8_t left_sample_face;
int current_id = -1; //Saber cual es la ultima cara reconocida
int unknown_count = 0;

//############################
//#######  Funciones  ########
//############################

bool faceIDInitFlash() {
  if (!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)) {
    Serial.println("SPIFFS Mount Failed");
    return false;
  }
  return true;
}

bool readFaceIDFromFlash(face_id_list *l) {
  File file = SPIFFS.open(FACE_ID_PATH);
  if (!file || file.isDirectory()) {
    Serial.println("No se pudo abrir el archivo de rostros");
    return false;
  }
  l->head = file.read();
  l->tail = file.read();
  l->count = file.read();
  l->size = file.read();
  l->confirm_times = file.read();
  const int block_len = FACE_ID_SIZE * sizeof(float);
  for (uint8_t i = 0; i < l->count; i++) {
    l->id_list[i] = dl_matrix3d_alloc(1, 1, 1, FACE_ID_SIZE);
    file.read((uint8_t*)l->id_list[i]->item, block_len);
  }
  file.close();
  return true;
}

bool writeFaceIDToFlash(face_id_list *l) {
  File file = SPIFFS.open(FACE_ID_PATH, FILE_WRITE);
  if (!file || file.isDirectory()) {
    Serial.println("No se pudo abrir el archivo para escritura");
    return false;
  }
  file.write(l->head);
  file.write(l->tail);
  file.write(l->count);
  file.write(l->size);
  file.write(l->confirm_times);
  const int block_len = FACE_ID_SIZE * sizeof(float);
  for (uint8_t i = 0; i < l->count; i++) {
    file.write((uint8_t*)l->id_list[i]->item, block_len);
  }
  file.close();
  return true;
}

mtmn_config_t get_mtmn_config() {
  mtmn_config_t config = {0};
  config.type = FAST;
  config.min_face = 80;
  config.pyramid = 0.707;
  config.pyramid_times = 4;
  config.p_threshold.score = 0.6;
  config.p_threshold.nms = 0.7;
  config.p_threshold.candidate_number = 20;
  config.r_threshold.score = 0.7;
  config.r_threshold.nms = 0.7;
  config.r_threshold.candidate_number = 10;
  config.o_threshold.score = 0.7;
  config.o_threshold.nms = 0.7;
  config.o_threshold.candidate_number = 1;
  return config;
}

int run_face_recognition(dl_matrix3du_t *image_matrix, box_array_t *net_boxes) {
  dl_matrix3du_t *aligned_face = NULL;
  int matched_id = -1;
  aligned_face = dl_matrix3du_alloc(1, FACE_WIDTH, FACE_HEIGHT, 3);
  if (!aligned_face) {
    Serial.println("Could not allocate face recognition buffer");
    return matched_id;
  }
  if (align_face(net_boxes, image_matrix, aligned_face) == ESP_OK) {
    if (is_enrolling == 1) {
      left_sample_face = enroll_face(&id_list, aligned_face);
      if (left_sample_face == (ENROLL_CONFIRM_TIMES - 1)) {
        Serial.printf("Enrolling Face ID: %d\n", id_list.tail);
      }
      Serial.printf("Enrolling Face ID: %d sample %d\n", id_list.tail, ENROLL_CONFIRM_TIMES - left_sample_face);
      if (left_sample_face == 0) {
        is_enrolling = 0;
        Serial.printf("Enrolled Face ID: %d\n", id_list.tail);
        writeFaceIDToFlash(&id_list);
      }
    } else {
      matched_id = recognize_face(&id_list, aligned_face);
      if (matched_id >= 0) {
        //Serial.printf("Match Face ID: %u\n", matched_id);
      } else {
        Serial.println("No Match Found");
        matched_id = -1;
      }
    }
  } else {
    Serial.println("Face Not Aligned");
  }
  dl_matrix3du_free(aligned_face);
  return matched_id;
}


//############################
//########  Arduino  #########
//############################

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);

  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;

  if (psramFound()) {
    config.frame_size = FRAMESIZE_QVGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_QVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
  mtmn_config = get_mtmn_config();

  face_id_init(&id_list, FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES);
  if (faceIDInitFlash()) {
    if (!readFaceIDFromFlash(&id_list)) {
      Serial.println("Carga de lista de rostros fallida, creando nueva.");
      id_list.head = id_list.tail = id_list.count = 0;
      id_list.size = FACE_ID_SAVE_NUMBER;
      id_list.confirm_times = ENROLL_CONFIRM_TIMES;
      for (int i = 0; i < id_list.size; i++) {
        id_list.id_list[i] = NULL;
      }
      if (writeFaceIDToFlash(&id_list)) {
        Serial.println("Archivo de Face ID creado.");
      } else {
        Serial.println("Error al crear archivo de Face ID");
      }
    } else {
      Serial.println("Carga de lista de rostros exitosa.");
    }
  } else {
    Serial.println("Error al montar SPIFFS");
  }
}

void loop() {
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Error al capturar imagen");
    return;
  }

  dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3);
  if (!image_matrix) {
    Serial.println("Fallo en la asignación de image_matrix");
    esp_camera_fb_return(fb);
    return;
  }

  if (!fmt2rgb888(fb->buf, fb->len, fb->format, image_matrix->item)) {
    Serial.println("Conversión a RGB888 fallida");
    dl_matrix3du_free(image_matrix);
    esp_camera_fb_return(fb);
    return;
  }

  box_array_t *net_boxes = face_detect(image_matrix, &mtmn_config);
  if (net_boxes) {
    int id = run_face_recognition(image_matrix, net_boxes);
    if (id >= 0) {
      if(id != current_id){
        Serial.printf("ID: %d\n", id);

        current_id = id;
      }else{
        Serial.println("\nCara actual");
      }
    } else {
      unknown_count++;
      if(unknown_count>=2){
        Serial.printf("NO ID\n");
        current_id = id;
      }
      
    }
    free(net_boxes->score);
    free(net_boxes->box);
    if (net_boxes->landmark) free(net_boxes->landmark);
    free(net_boxes);
  } else {
    Serial.println("No se detectaron caras.");
  }

  dl_matrix3du_free(image_matrix);
  esp_camera_fb_return(fb);
  delay(5000);
}

Base de datos local de las canciones:

// Notas musicales a través de tonos en Arduino 
#define D3 147
#define A3 220
#define C4 262
#define D4 294
#define E4 330
#define F4 349
#define G4 392
#define Ab3 233
#define F3 175
#define E3 165
#define C5 523
#define E5 659
#define G5 784
#define G3 196
#define A4 440
#define B4 494
#define Ab4 466
#define A5 880
#define F5 698
#define D5 587
#define Fb4 370
#define Db4 311
#define Gb3 208

// Melodía de Piratas del Caribe (melodía 1) - Versión acortada por falta de espacio de Arduino
const uint16_t MELODIA1[100][3] = {
 {D3, 273, 0},
 {D3, 136, 0},
 {D3, 273, 0},
 {D3, 136, 0},
 {D3, 273, 0},
 {D3, 136, 0},
 {D3, 136, 0},
 {D3, 136, 0},
 {D3, 136, 0},
 {D3, 273, 0},
 {D3, 136, 0},
 {D3, 273, 0},
 {D3, 136, 0},
 {D3, 273, 0},
 {D3, 136, 0},
 {D3, 136, 0},
 {D3, 136, 0},
 {D3, 136, 0},
 {D3, 409, 136},
 {A3, 136, 0},
 {C4, 136, 0},
 {D4, 273, 0},
 {D4, 273, 0},
 {D4, 136, 0},
 {E4, 136, 0},
 {F4, 273, 0},
 {F4, 273, 0},
 {F4, 136, 0},
 {G4, 136, 0},
 {E4, 273, 0},
 {E4, 273, 0},
 {D4, 136, 0},
 {C4, 136, 0},
 {C4, 136, 0},
 {D4, 273, 136},
 {A3, 136, 0},
 {C4, 136, 0},
 {D4, 273, 0},
 {D4, 273, 0},
 {D4, 136, 0},
 {E4, 136, 0},
 {F4, 273, 0},
 {F4, 273, 0},
 {F4, 136, 0},
 {G4, 136, 0},
 {E4, 273, 0},
 {E4, 273, 0},
 {D4, 136, 0},
 {C4, 136, 0},
 {D4, 409, 136},
 {A3, 136, 0},
 {C4, 136, 0},
 {D4, 273, 0},
 {D4, 273, 0},
 {D4, 136, 0},
 {F4, 136, 0},
 {G4, 273, 0},
 {G4, 273, 0},
 {G4, 136, 0},
 {A4, 136, 0},
 {Ab4, 273, 0},
 {Ab4, 273, 0},
 {A4, 136, 0},
 {G4, 136, 0},
 {A4, 136, 0},
 {D4, 273, 136},
 {D4, 136, 0},
 {E4, 136, 0},
 {F4, 273, 0},
 {F4, 273, 0},
 {G4, 273, 0},
 {A4, 136, 0},
 {D4, 273, 136},
 {D4, 136, 0},
 {F4, 136, 0},
 {E4, 273, 0},
 {E4, 273, 0},
 {F4, 136, 0},
 {D4, 136, 0},
 {E4, 409, 136},
 {A3, 136, 0},
 {C4, 136, 0},
 {D4, 273, 0},
 {D4, 273, 0},
 {D4, 136, 0},
 {E4, 136, 0},
 {F4, 273, 0},
 {F4, 273, 0},
 {F4, 136, 0},
 {G4, 136, 0},
 {E4, 273, 0},
 {E4, 273, 0},
 {D4, 136, 0},
 {C4, 136, 0},
 {C4, 136, 0},
 {D4, 273, 136},
 {A3, 136, 0},
 {C4, 136, 0},
 {D4, 273, 0},
 {D4, 273, 0},
};

// Melodía de Super Mario Bros (melodía 2) - Versión acortada por falta de espacio de Arduino
const uint16_t MELODIA2[100][3] = {
 {E5, 63, 125},
 {E5, 63, 250},
 {E5, 63, 250},
 {C5, 63, 63},
 {E5, 63, 250},
 {G5, 63, 500},
 {G3, 188, 563},
 {C5, 500, 0},
 {G4, 375, 63},
 {E4, 250, 0},
 {D3, 250, 0},
 {A4, 250, 63},
 {B4, 250, 63},
 {Ab4, 188, 0},
 {A4, 250, 63},
 {G4, 125, 0},
 {E5, 250, 0},
 {G5, 250, 0},
 {A5, 250, 63},
 {F5, 188, 0},
 {G5, 250, 63},
 {E5, 250, 63},
 {C5, 188, 0},
 {D5, 125, 0},
 {B4, 500, 0},
 {C5, 250, 0},
 {G3, 63, 188},
 {G4, 375, 63},
 {E4, 500, 0},
 {A4, 250, 63},
 {B4, 250, 63},
 {Ab4, 188, 0},
 {A4, 63, 250},
 {G4, 125, 0},
 {E5, 250, 0},
 {G5, 250, 0},
 {A5, 250, 63},
 {F5, 188, 0},
 {G5, 250, 63},
 {E5, 250, 63},
 {C5, 188, 0},
 {D5, 125, 0},
 {B4, 750, 63},
 {G4, 125, 0},
 {Fb4, 188, 0},
 {F4, 188, 0},
 {Db4, 125, 0},
 {Db4, 188, 0},
 {E4, 63, 63},
 {E4, 188, 0},
 {Gb3, 125, 0},
 {A3, 188, 0},
 {C4, 188, 0},
 {C4, 125, 0},
 {A3, 188, 0},
 {C4, 125, 0},
 {D4, 188, 0},
 {D4, 188, 125},
 {G4, 125, 0},
 {Fb4, 188, 0},
 {F4, 188, 0},
 {Db4, 125, 0},
 {Db4, 188, 0},
 {E4, 125, 188},
 {G4, 125, 188},
 {G4, 125, 0},
 {G4, 188, 813},
 {G4, 125, 0},
 {Fb4, 188, 0},
 {F4, 188, 0},
 {Db4, 125, 188},
 {E4, 125, 188},
 {Gb3, 125, 0},
 {A3, 188, 0},
 {C4, 125, 188},
 {A3, 188, 0},
 {C4, 125, 0},
 {D4, 188, 313},
 {Db4, 125, 313},
 {D4, 188, 313},
 {C4, 188, 1125},
 {A4, 125, 0},
 {C5, 250, 63},
 {C5, 250, 63},
 {C5, 188, 0},
 {D5, 250, 63},
 {E5, 188, 0},
 {C5, 250, 63},
 {A4, 125, 0},
 {G4, 500, 188},
 {A4, 125, 0},
 {C5, 250, 63},
 {C5, 250, 63},
 {C5, 188, 0},
 {D5, 250, 63},
 {E5, 1000, 313},
 {A4, 125, 0},
 {C5, 250, 63},
 {C5, 250, 63},
 {C5, 188, 0},
};

Reparto de tareas

El reparto de tareas en ocasiones no ha sido significativo, puesto a que todos hemos trabajado cooperativamente en la creación y sugerencia de ideas, la búsqueda de elementos y componentes que necesitábamos para el espejo inteligente, la creación de ideas para la implementación de ideas, la documentación necesaria para el blog… Sin embargo, al ser tres personas y al haber tres partes diferenciadas en la estructura del proyecto como ya hemos dicho antes (pantalla y cámara, iluminación y sonido), cada uno de nosotros se ha dedicado a una parte. Además de esto, uno tuvo que realizar la unión de las tres partes de la estructura y la presentación del espejo inteligente (como se iba a ver), por lo que los otros dos hicieron la documentación y el PowerPoint para la presentación presencial del proyecto.

Problemas encontrados

Problema con correlación de valores RGB

La tira led utilizada era de ánodo común en vez de cátodo común por lo que a la hora de darle valores a cada color había que darle el valor inverso. Por ejemplo, si querías el máximo de color azul necesitabas indicarle valor 0 al color azul.

La solución encontrada fue crear una función que para cada color invierta el valor de este, haciendo que el color representado en la tira led se corresponda con el color buscado.

Problema con funcionamiento del sensor

El sensor ESP32-CAM que compramos al principio era incompatible con el formato de imagen. Después de un exhaustivo análisis descubrimos que el problema provenía de que uno de los componentes de este sensor estaba defectuoso, por lo que la solución fue buscar otro sensor que no tuviera dicho problema y juntarlo con la cámara.

Problema al unir código

En el momento en el que mezclamos el código para juntar las distintas partes nos dimos cuenta de que al pulsar el botón de cambiar de color también se reproducía la música, provocando un comportamiento inesperado. Se ha añadido un booleano (variable reproducir explicada anteriormente en la programación del sonido) para que cuando se pare la música no bloquee el resto de las funcionalidades.

Fases del funcionamiento de AuraView

El funcionamiento de nuestro espejo inteligente AuraView tiene distintas fases para cada parte (pantalla y cámara, iluminación y sonido) siendo la pantalla y cámara la más importante:

Pantalla y cámara

  1. El usuario se acerca al espejo.
  2. La cámara intenta detectar al usuario.
  3. Si el usuario no está registrado, la iluminación parpadea de color rojo en señal de que no ha reconocido al usuario. La cámara puede registrar a dicho usuario para mostrarle una interfaz personalizada, pero hasta que no lo detecte, no pasará a la siguiente fase. Si el usuario ya está registrado, la iluminación parpadea de color verde en señal de que si ha reconocido al usuario y se pasa a la siguiente fase.
  4. La pantalla genera la interfaz del usuario que puede personalizar a su gusto con su calendario, noticias, el tiempo y la hora de su ubicación… Si se va el usuario, se vuelve al paso 1.

Iluminación

  1. El usuario puede encender y apagar la tira led en cualquier momento.
  2. Si la tira led está encendida, esta puede cambiar de color dándole al botón correspondiente. Tiene varias opciones (azul, amarillo, azul claro, morado, blanco, rojo y verde).

Sonido

  1. El usuario puede reproducir o no música en cualquier momento.
  2. Si el altavoz está reproduciendo música, el usuario puede cambiar a la siguiente canción de la lista.

En el siguiente caso de uso ponemos resto de canciones porque en el código se pueden poner más canciones de las que tenemos que son dos.

Video enseñando el funcionamiento

También te podría gustar...

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *