TinyML - Personal Trainer usando inteligência artificial na borda

Participantes:

João Vitor Yukio Bordin Yamashita

Resumo do projeto:

Foi utilizado dados de um acelerômetro e inteligência artificial (IA) embarcada para classificar se um exercício feito usando um haltere foi feito de forma correta ou incorreta. Obteve-se uma precisão de 92,6% para o movimento "rosca direta".

Descrição do projeto:

Coleta de dados

Os dados foram coletados usando o acelerômetro interno do MPU6050 e o acelerômetro LSM9DS1 do Arduino Nano 33 BLE Sense, os sensores foram fixados em halteres de 10 kg, como pode ser visto na Figura 1, e foram realizados os movimentos de uma rosca direta feita de forma correta e movimentos de uma rosca direta feita de forma incorreta, como podemos observar na Figura 2. Esses dados foram enviados para plataforma de aquisição de dados da Edge Impulse para serem processados.

Figura 1 – Arduino Nano 33 BLE Sense fixado no haltere.

Figura 2 – Aquisiçao de dados e upload para plataforma Edge Impulse.

Para coletar os dados usando a Franzininho ou qualquer outra placa de desenvolvimento não suportada diretamente para aquisição de dados podemos utilizar a ferramenta de Data Forwarder disponível nos tutoriais da Edge Impulse (na seção de software foi disponibilizado um código utilizado para aquisição dos dados com a Franzininho).

Processamento dos dados

Podemos ver um exemplo de dado adquirido na etapa anterior nas Figuras 3 e 4.

Figura 3 – Dados do acelerômetro referentes ao movimento feito de forma correta

Figura 4 – Dados do acelerômetro referentes ao movimento feito de forma incorreta

Esses dados são filtrados por um filtro passa-baixas para eliminar possíveis ruídos de alta frequência presente nos dados, como podemos ver na Figura 5.

Figura 5 – Na imagem superior podemos ver o sinal “cru” e na figura inferior o mesmo sinal após ser filtrado por um filtro passa-baixas.

Após a filtragem do sinal são feitas duas análises espectrais, a primeira é a Fast Fourier Transform (FFT) que “quebra” o sinal em uma soma de várias frequências e a segunda análise é a Spectral Power Distribution (SPD) que consiste em encontrar a potência distribuída em cada faixa de frequência do sinal. Podemos ver um exemplo de saída do sinal usando a FFT e SPD na Figura 6

Figura 6 – O mesmo sinal após o processo da FFT e SPD

Cada coeficiente dessas análises vão ser usados como feature para a rede neural que irá classificar o movimento.

Arquitetura da rede e treinamento

Como foi mencionado anteriormente os coeficientes obtidos das análises são os inputs da nossa rede neural. Foram testados algumas arquiteturas de rede, mas a que obteve um bom equilibro entre tamanho (em termos de parâmetros) e performance foi a rede com três camadas  densas  com 20 neurônios em cada. A rede pode ser observada na Figura 7.

Figura 7 – Arquitetura do modelo.

Com a arquitetura definida foi realizado o treinamento com 30 epochs e os resultados se mostraram muito satisfatórios, com precisão de 92,6%. Além disso conseguimos observar na Figura 8, que não tivemos overfitting já que a curva da precisão e loss dos dados de validação seguiram as curvas dos dados de treinamento.

Figura 8 – Curvas de treinamento do modelo.

Além da rede densa, foi utilizado um detector de anomalias, utilizando o método k-means. Esse método agrupa, no nosso caso, os valores RMS dos eixos em clusters, como podemos ver na Figura 9, caso algum valor esteja fora desses clusters ele é classificado como uma anomalia.

Figura 9 – Clusters criados usando os valores RMS dos três eixos do acelerômetro.

A matriz de confusão dos dados de validação pode ser observada abaixo.

Matriz de confusão dos dados de validação.

Sistema embarcado

O conjunto do sensor com a Franzinho pode ser observado na Figura 10.

Figura 10 – Franzinho WiFi com o sensor MPU 6050

Foi feito o download do modelo na plataforma Edge Impulse para a plataforma Arduino, esse arquivo pode ser usado como uma biblioteca genérica, apenas é necessário garantir que os dados que vão ser utilizados no modelo estão “formatados” da mesma forma que no treinamento. Após o programa ser gravado na placa é necessário fixar a placa com o sensor no haltere, como pode ser visto na Figura 11, e realizar os testes. Para visualização do usuário foi utilizada a luz verde para um movimento correto, vermelha para o incorreto e azul caso seja detectado que o haltere está parado.

Figura 11 – Sistema completo com haltere.

Os resultados podem ser vistos no vídeo abaixo, nesse vídeo realizei o teste das três classes, idle, correto e incorreto.

Foi possível implementar um sistema para classificar se o movimento feito em um exercício usando haltere foi feito de forma correta ou não, se for desenvolvido de forma mais aprofundada pode ser feito algum tipo de adaptador que pode ser impresso usando uma impressora 3d para ser utilizado em uma escala maior, o procedimento para outros tipos de exercício é o mesmo, um GitHub foi feito com todos os códigos utilizados e também do trabalho desenvolvido usando o Arduino Nano 33 BLE Sense, onde é discutido um pouco mais a fundo o tradeoff de se utilizar vários exercícios em um mesmo modelo, mas é possível ter dois ou mais exercícios em um mesmo modelo se a redução da precisão for aceitável.

 

Histórico do desenvolvimento:

03/jul/2021 – Início do planejamento do projeto final do curso da Unifei.

12/jul/2021 – Ideia do projeto finalizada e conversa com academia local para uso dos halteres e opinião dos professores.

21/jul/2021 – Aquisição de dados na academia

26/jul/2021 – Processamento dos dados

08/ago/2021 – Treinamento do modelo

09/ago/2021 – Embarcar o modelo na placa Arduino Nano 33 BLE Sense

11/ago/2021 – Avaliação da performance embarcada

18/ago/2021 – Elaboração do artigo

09/nov/2021 – Inicio planejamento adaptações para Franzininho WiFi

18/nov/2021 – Finalização do planejamento e compra dos componentes necessários

25/nov/2021 – Finalização das adaptações na Frazininho WiFi, tanto em relação à software quanto ao hardware

17/dez/2021 – Inicio escrita do conteúdo para o projeto

27/dez/2021 – Teste preliminar do código e funcionamento do sistema

27/dez/2021 – Finalização do protótipo com halter

Hardware:

Conexão MPU6050 – Franzininho WiFi

MPU6050 SCL -> Pino 9 Franzininho WiFi

MPU6050 SDA -> Pino 8 Franzininho WiFi

MPU6050 GND -> GND

MPU6050 VCC -> 3V3

Conexão Power Bank – Franzininho WiFi

5v Power Bank -> 5v Franzininho WiFi

GND Power Bank -> GND Franzininho WiFi

Software/Firmware:

Código referente ao uso da ferramenta Data Forwarder da Edge Impulse

#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>

#define FREQUENCY_HZ 60
#define INTERVAL_MS (1000 / (FREQUENCY_HZ + 1))

// objeto da classe Adafruit_MPU6050
Adafruit_MPU6050 mpu;

static unsigned long last_interval_ms = 0;

void setup() {
Serial.begin(115200);
Serial.println(“Classificador de gestos com TinyML”);

// Try to initialize!
if (!mpu.begin()) {
Serial.println(“Failed to find MPU6050 chip”);
while (1) {
delay(10);
}
}
Serial.println(“MPU6050 Found!”);

mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
Serial.print(“Accelerometer range set to: “);
switch (mpu.getAccelerometerRange()) {
case MPU6050_RANGE_2_G:
Serial.println(“+-2G”);
break;
case MPU6050_RANGE_4_G:
Serial.println(“+-4G”);
break;
case MPU6050_RANGE_8_G:
Serial.println(“+-8G”);
break;
case MPU6050_RANGE_16_G:
Serial.println(“+-16G”);
break;
}
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
Serial.print(“Gyro range set to: “);
switch (mpu.getGyroRange()) {
case MPU6050_RANGE_250_DEG:
Serial.println(“+- 250 deg/s”);
break;
case MPU6050_RANGE_500_DEG:
Serial.println(“+- 500 deg/s”);
break;
case MPU6050_RANGE_1000_DEG:
Serial.println(“+- 1000 deg/s”);
break;
case MPU6050_RANGE_2000_DEG:
Serial.println(“+- 2000 deg/s”);
break;
}

mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
Serial.print(“Filter bandwidth set to: “);
switch (mpu.getFilterBandwidth()) {
case MPU6050_BAND_260_HZ:
Serial.println(“260 Hz”);
break;
case MPU6050_BAND_184_HZ:
Serial.println(“184 Hz”);
break;
case MPU6050_BAND_94_HZ:
Serial.println(“94 Hz”);
break;
case MPU6050_BAND_44_HZ:
Serial.println(“44 Hz”);
break;
case MPU6050_BAND_21_HZ:
Serial.println(“21 Hz”);
break;
case MPU6050_BAND_10_HZ:
Serial.println(“10 Hz”);
break;
case MPU6050_BAND_5_HZ:
Serial.println(“5 Hz”);
break;
}

Serial.println(“”);
delay(100);
}

void loop() {

sensors_event_t a, g, temp;

if (millis() > last_interval_ms + INTERVAL_MS) {
last_interval_ms = millis();

mpu.getEvent(&a, &g, &temp);

Serial.print(a.acceleration.x);
Serial.print(“,”);
Serial.print(a.acceleration.y);
Serial.print(“,”);
Serial.println(a.acceleration.z);

}

}

 

Código referente a inferência do modelo na Franzininho WiFi

#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
#include <dumbell_class_inferencing.h>
#include <Adafruit_NeoPixel.h>

#define FREQUENCY_HZ 100
#define INTERVAL_MS (1000 / (FREQUENCY_HZ + 1))

// objeto da classe Adafruit_MPU6050
Adafruit_MPU6050 mpu;

float features[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];
size_t feature_ix = 0;

static unsigned long last_interval_ms = 0;

#define PIN 18
#define NUMPIXELS 1

#define RED pixels.Color(255, 0, 0) // vermelho
#define GREEN pixels.Color(0, 255, 0) // verde
#define BLUE pixels.Color(0, 0, 255) // azul
#define WHITE pixels.Color(255, 255, 255) // branco

Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

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

pixels.begin();

if (!mpu.begin()) {
Serial.println(“Failed to find MPU6050 chip”);
while (1) {
delay(10);
}
}
Serial.println(“MPU6050 Found!”);

mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
Serial.print(“Accelerometer range set to: “);
switch (mpu.getAccelerometerRange()) {
case MPU6050_RANGE_2_G:
Serial.println(“+-2G”);
break;
case MPU6050_RANGE_4_G:
Serial.println(“+-4G”);
break;
case MPU6050_RANGE_8_G:
Serial.println(“+-8G”);
break;
case MPU6050_RANGE_16_G:
Serial.println(“+-16G”);
break;
}
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
Serial.print(“Gyro range set to: “);
switch (mpu.getGyroRange()) {
case MPU6050_RANGE_250_DEG:
Serial.println(“+- 250 deg/s”);
break;
case MPU6050_RANGE_500_DEG:
Serial.println(“+- 500 deg/s”);
break;
case MPU6050_RANGE_1000_DEG:
Serial.println(“+- 1000 deg/s”);
break;
case MPU6050_RANGE_2000_DEG:
Serial.println(“+- 2000 deg/s”);
break;
}

mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
Serial.print(“Filter bandwidth set to: “);
switch (mpu.getFilterBandwidth()) {
case MPU6050_BAND_260_HZ:
Serial.println(“260 Hz”);
break;
case MPU6050_BAND_184_HZ:
Serial.println(“184 Hz”);
break;
case MPU6050_BAND_94_HZ:
Serial.println(“94 Hz”);
break;
case MPU6050_BAND_44_HZ:
Serial.println(“44 Hz”);
break;
case MPU6050_BAND_21_HZ:
Serial.println(“21 Hz”);
break;
case MPU6050_BAND_10_HZ:
Serial.println(“10 Hz”);
break;
case MPU6050_BAND_5_HZ:
Serial.println(“5 Hz”);
break;
}

Serial.println(“”);
delay(100);

Serial.print(“Features: “);
Serial.println(EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
Serial.print(“Label count: “);
Serial.println(EI_CLASSIFIER_LABEL_COUNT);

}

void loop() {
sensors_event_t a, g, temp;

if (millis() > last_interval_ms + INTERVAL_MS) {
last_interval_ms = millis();

mpu.getEvent(&a, &g, &temp);

features[feature_ix++] = a.acceleration.x;
features[feature_ix++] = a.acceleration.y;
features[feature_ix++] = a.acceleration.z;

if (feature_ix == EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
Serial.println(“Running the inference…”);
signal_t signal;
ei_impulse_result_t result;
int err = numpy::signal_from_buffer(features, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
if (err != 0) {
ei_printf(“Failed to create signal from buffer (%d)\n”, err);
return;
}

EI_IMPULSE_ERROR res = run_classifier(&signal, &result, true);

if (res != 0) return;

ei_printf(“Predictions “);
ei_printf(“(DSP: %d ms., Classification: %d ms.)”,
result.timing.dsp, result.timing.classification);
ei_printf(“: \n”);

for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
ei_printf(” %s: %.5f\n”, result.classification[ix].label, result.classification[ix].value);
if (result.classification[ix].value > 0.7) { //caso o modelo tenha mais de 70% do movimento
if (result.classification[ix].label == “correto”) { // caso seja um movimento correto
pixels.setPixelColor(0, GREEN);
pixels.show(); // envia o pixel atualizado para o hardware
} else if (result.classification[ix].label == “idle”) { //caso classifique como idle
pixels.setPixelColor(0, BLUE);
pixels.show(); // envia o pixel atualizado para o hardware
} else { // caso seja incorreto
pixels.setPixelColor(0, RED);
pixels.show(); // envia o pixel atualizado para o hardware
}
}
}
feature_ix = 0;
}

}
}

void ei_printf(const char *format, …) {
static char print_buf[1024] = { 0 };

va_list args;
va_start(args, format);
int r = vsnprintf(print_buf, sizeof(print_buf), format, args);
va_end(args);

if (r > 0) {
Serial.write(print_buf);
}
}

Referências:

Guias Franzininho WiFi:

https://docs.franzininho.com.br/docs/franzininho-wifi/exemplos-arduino/primeiros-passos

https://docs.franzininho.com.br/docs/franzininho-wifi/exemplos-arduino/neopixel-onboard

Guias Edge Impulse:

https://docs.edgeimpulse.com/docs/cli-data-forwarder

Uso Esp32 com Edge Impulse:

https://www.hackster.io/Yukio/gesture-classification-with-esp32-and-tinyml-dab252

GitHub do projeto:

https://github.com/JoaoYukio/PersonalTrainer-FranzininhoWiFi