TinyML – reconhecimento de movimento usando Raspberry Pi Pico

Tempo de leitura: 18 minutes

Neste tutorial, usaremos o aprendizado de máquina para construir um sistema de reconhecimento de gestos que roda em um microcontrolador minúsculo, o RP2040.

Este tutorial tem 2 partes. A primeira é explorar o Raspberry Pi Pico, seus principais componentes e como programá-lo usando o Micropython e seu C/C++ SDK (Software Development Kit).

A seguir, usaremos o Pico para capturar “dados de gestos” para serem usados em um treinamento de modelo TinyML, usando o Edge Impulse Studio. Depois de desenvolvido e testado, o modelo será implantado e usado para inferência real no mesmo dispositivo.

Se você está familiarizado com a programação básica do Pico, sinta-se à vontade para pular para a parte 2, onde a verdadeira diversão começará!

 

PARTE 1: Explorando o Raspberry Pi Pico e seu SDK

The Raspberry Pi Pico

Raspberry Pi Pico é uma placa microcontrolada de baixo custo e alto desempenho com interfaces digitais flexíveis. Os principais recursos incluem:

  • Chip microcontrolador RP2040 projetado pela Raspberry Pi Foundation
  • Processador Dual-core Arm Cortex M0 +, clock flexível rodando até 133 MHz
  • 264 KB de SRAM e 2 MB de memória Flash on-board
  • USB 1.1 com dispositivo e suporte de host
  • Modos inativos e de baixo consumo de energia
  • 26 × pinos GPIO multifuncionais
  • 2 × SPI, 2 × I2C, 2 × UART, 3 × ADC de 12 bits, 16 × canais PWM controláveis
  • Relógio preciso e temporizador no chip
  • Sensor de temperatura
  • Bibliotecas de ponto flutuante aceleradas no chip
  • 8 × máquinas de estado de E/S programável (PIO) para suporte periférico personalizado

Uma característica interessante é a capacidade de arrastar e soltar a programação usando armazenamento em massa via USB.

Apesar de ser simples “carregar” um programa para o Pico; o que falta é um botão de reinicialização para evitar a desconexão do USB sempre que um novo código é carregado, o que pode danificar o conector USB do Pico. Felizmente, o pino 30 (RUN) está disponível e pode ser usado para esta função. Basta usar um botão (normalmente aberto), conectando este pino ao solo. Agora, sempre que um programa deve ser carregado para o Pico, pressione os dois botões ao mesmo tempo.

Neste link de documentação, é possível encontrar informações detalhadas sobre o MCU RP 2040, o coração do Pico.

 

Programação do Pico

Existem duas maneiras de programar um Pico: MicroPython e C/C++.

Programação usando MicroPython

Para os testes iniciais (e para iniciantes), executar o MicroPython com o Pico é extremamente fácil. Uma vez que o Pico é conectado pela primeira vez em seu computador (via USB) e com o botão BOOT pressionado (ou pressionando Reset e Boot, após a conexão), uma janela chamada RPI-RP2 deve aparecer, como um Dispositivo de Armazenamento em Massa normal (o mesmo como um Pen-Driver regular).

Clicar em INDEX.HTM o direcionará para uma página onde você iniciará o MicroPython.

Siga as instruções, baixando o arquivo UF2 que irá instalar facilmente o interpretador MicroPython no Pico. Tendo o arquivo UF2, arraste-o apenas para aquela janela RPI-RP2 e pronto! O Pico está pronto para receber um script Python executável. Para MicroPython, sugiro Thonny como IDE de escolha, uma vez que é possível escrever scripts python diretamente no shell como abaixo, ou desenvolver um script no editor:

Confirme se o intérprete está configurado para o Pico. Clique nele (canto direito inferior do IDE) para ver as opções.

Também é possível escrever ou entrar com scripts Python conforme o exemplo intermitente abaixo:

Com o botão Executar (marcado na figura acima), o script é carregado para o Pico. O LED interno (pino 25) piscará 10 vezes, imprimindo o número do loop no Shell.

Tente agora ler o sensor de temperatura interno, criando um arquivo de log para monitorar a temperatura interna do Pico. Neste exemplo, o arquivo de log temp.txt é armazenado dentro do Pico, então preste atenção em quanto espaço de memória você precisará.

Se você é novo no MicroPython, a Raspberry Pi Foundation elaborou um excelente livro, Get Started with MicroPython on Raspberry Pi Pico (grátis em pdf), que ensinará todos os passos da computação física usando o Pico e MicroPython.

 

Programação em C/C++

MicroPython é bom para exploração inicial e aprendizado de eletrônica, mas é importante usar a linguagem C/C++ para projetos embarcados reais. Para isso, é necessário entender o SDK C/C++.

A Fundação RPi reuniu uma grande documentação. O primeiro: Introdução ao Raspberry Pi Pico fornece informações sobre como configurar seu hardware, IDE / ambiente e como construir e depurar software para o Raspberry Pi Pico (e outros dispositivos baseados em RP2040).

O segundo documento, Raspberry Pi Pico C/C++ SDK, explora a programação usando o SDK com recursos avançados e fornece documentação API completa.

Instalação do SDK com Linux:

Instale ferramentas (Cmake e gcc para ARM):

sudo apt update
sudo apt install git cmake gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential

Crie uma pasta, onde serão desenvolvidos os projetos:

cd ~/
mkdir pico
cd pico

Clone o repositório SDK:

git clone -b master https://github.com/raspberrypi/pico-sdk.git

Vá para a pasta pico-sdk e atualize os submódulos:

cd pico-sdk
git submodule update --init

Voltar para a pasta pico

cd ..

Instalação do SDK com MacOSInstall Toolkit

brew install cmake
brew tap ArmMbed/homebrew-formulae
brew install arm-none-eabi-gcc

Instale o SDK

cd ~/
mkdir pico
cd pico
git clone -b master https://github.com/raspberrypi/pico-sdk.git
cd pico-sdk
git submodule update --init
cd ..

Neste ponto, você está pronto para criar um projeto incorporado usando C/C++

 

Criando um projeto Blink em C/C++

É bom fazer o download dos exemplos criados especificamente para o Pico. Eles nos darão um bom ponto de partida para lidar com o HW e as bibliotecas.

git clone -b master https://github.com/raspberrypi/pico-examples.git

Os exemplos também têm um código intermitente, mas vamos começar um completo do zero. Primeiro, crie uma pasta onde seu projeto estará localizado (em / pico / e no mesmo nível em que seu / pico-sdk está localizado:

cd~/
cd pico
mkdir blink
cd blink
mkdir build

Observe que também criamos uma subpasta chamada build. Esta pasta receberá o código compilado final para ser carregado no Pico. Na pasta do projeto (neste caso, piscando), devemos sempre ter 3 arquivos:

  • blink.c (o código C principal)
  • CMakeList.txt (que diz ao SDK como transformar o arquivo C em um aplicativo binário para uma placa microcontrolada baseada em RP2040)
  • pico_sdk_import.cmake (ajuda para localizar o SDK)

Vamos começar a copiar pico_sdk_import.cmake na pasta do projeto:

cp ../pico-sdk/external/pico_sdk_import.cmake .

Para blink.c (arquivo de origem C) e CMakeList.txt, use um editor de texto que você goste mais, como Eclipse, VS, Geany. Vejamos o CMakeList.txt:

cmake_minimum_required(VERSION 3.12)
 
project(app_blink_project)
 
include(pico_sdk_import.cmake)
pico_sdk_init()
 
add_executable(app_blink
    blink.c
)
 
pico_add_extra_outputs(app_blink)
target_link_libraries(app_blink pico_stdlib)

Observe que criaremos um arquivo executável chamado: app_blink com base no código de blink.k Agora, o código-fonte, blink.c:

/**
 * Pico - RP2040
 * Blink Internal LED
 */
 
#include "pico/stdlib.h"
 
const uint LED_PIN = 25;
 
int main() {
 
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    gpio_put(LED_PIN, 0);
     
    while (true) {
        gpio_put(LED_PIN, 1);
        sleep_ms(250);
        gpio_put(LED_PIN, 0);
        sleep_ms(250);
    }
}

No topo do arquivo C, incluímos um cabeçalho chamado pico/stdlib.h. Este é um cabeçalho guarda-chuva que puxa alguns outros cabeçalhos comumente usados. Os necessários aqui são hardware/gpio.h, que é usado para acessar os IOs de uso geral no RP2040 (as funções gpio_xxx aqui) e pico/time.h, que contém, entre outras coisas, a função sleep_ms.

Em termos gerais, uma biblioteca cujo nome começa com pico fornece APIs e conceitos de alto nível ou agrega interfaces menores; um nome que começa com hardware indica uma abstração mais fina entre seu código e o hardware RP2040 no chip.

Então, usando principalmente as bibliotecas hardware_gpio e pico_time, este programa C piscará um LED conectado ao GPIO25 ligado e desligado, duas vezes por segundo, para sempre (ou pelo menos até ser desconectado).

Excelente! Neste ponto, a pasta do seu projeto deve conter 3 arquivos e uma subpasta (build):

pico/
├── blink/
│   ├── blink.c
│   ├── CMakeLists.txt  
│   ├── pico_sdk_import.cmake
│   └── build/
│
├── pico-sdk/
│   ├── cmake
│   └── external
│   └── ...

Agora, vá para a pasta build, exporte as variáveis de ambiente e execute cmake:

cd build
export PICO_SDK_PATH=../../pico-sdk
cmake ..

A última etapa é compilar o projeto:

make -j4

Na pasta de construção, vários arquivos são gerados, incluindo o app_build.uf2, o arquivo executável.

Pressione Boot e Reset para abrir a janela RPI-RP2 e arraste o arquivo de projeto compilado app_build.uf2 para essa pasta.

Você também pode usar cp na linha de comando, em vez de arrastar o arquivo.

Apesar de todas essas etapas parecerem complicadas, uma vez que seu ambiente de projeto está configurado, para qualquer mudança no projeto, você só deve compilar o novo código usando make ou make -j4 (que usou todos os 4 núcleos da CPU).

 

PARTE 2: TinyML – Projeto de reconhecimento de movimento usando o Pico

A ideia deste projeto é usar o Pico para classificar alguns gestos feitos pelo homem como “cima-baixo”, “esquerda-direita” e “círculo”. Essa classificação será feita 100% “off-line” no nível do MPU. Em outras palavras, o que estaremos fazendo é “aprendizado de máquina embarcado, também conhecido como TinyML.

Conforme explicado na documentação do Edge Impulse (o que é ML incorporado, afinal?), Os avanços recentes na arquitetura do microprocessador e no design de algoritmo tornaram possível executar cargas de trabalho de aprendizado de máquina sofisticadas até mesmo nos menores microcontroladores (nosso caso com RP2040).

Dependendo basicamente da capacidade de HW e do tamanho da memória, diferentes tipos de MCU/Aplicativo podem ser usados na arena TinyML, conforme mostrado no gráfico abaixo.

Nosso Pico, baseado no ARM Cortex-M0 + é mais do que adequado para realizar a Classificação de Sensores, como faremos neste projeto.

 

O fluxo de trabalho do aprendizado de máquina

Até agora, já definimos a 1ª fase do projeto: o seu objetivo (Classificação de Gestos). O workflow abaixo mostra todas as fases restantes a serem executadas desde a coleta de dados em nosso Pico até a inferência final e avaliação de volta ao nosso minúsculo dispositivo, passando pelo desenvolvimento do modelo real feito no Edge Impulse Studio na nuvem.

 

Colete um conjunto de dados

O TinyML permite a inteligência da máquina ao lado do mundo físico, o que significa cerca de sensores. Então, a primeira coisa a fazer é capturar dados para entender esses gestos. Para isso, usaremos um acelerômetro simples de 3 eixos.

O Sensor

O sensor usado, o MMA7361L, é um acelerômetro analógico de três eixos que requer uma quantidade escassa de energia e possui uma entrada g-select que alterna o acelerômetro entre as faixas de medição de ±1,5g e ±6g. Outros recursos incluem um modo de espera, condicionamento de sinal, um filtro passa-baixo de 1 pólo, compensação de temperatura, autoteste e detecção de 0g, que detecta queda livre linear. A sensibilidade e o deslocamento zero-g são definidos de fábrica e não requerem dispositivos externos.

Cada saída analógica do sensor (XOUT, YOUT e ZOUT) será conectada às entradas ADC do Pico (ADC0, 1 e 2). O VDD é de 3,3V e também será fornecido pela Pico. O pino GS selecionou o nível g e ficará aberto (+/-1,5G).

 

Diagrama

Existem vários pacotes diferentes para acelerômetro analógico de três eixos. Em princípio, qualquer placa auxiliar para o MMA7361L da Freescale deve funcionar.

 

Medições do sensor:

Com o pino GS deixado aberto (+/-1,5G), a sensibilidade do sensor de acordo com as especificações é: 800mV/g, sendo que para 1G (sensor em repouso), a saída é em torno de 1,65V (‘G0’). É importante lembrar que os ADCs do Pico possuem uma resolução de 12bits (3,3V ==> 4096), então se quisermos a medição do ADC em g, devemos aplicar o seguinte fator de conversão aos dados brutos coletados (read_axis_raw):

conversion_factor = 3.3V / 4096
read_axis_in_g = (read_axis_raw * conversion_factor) - G0

e para aceleração em m/s:

CONVERT_G_TO_MS2 = 9.80665
read_axis_in_ms = read_axis_in_g  * CONVERT_G_TO_MS2

Preparando o ambiente do projeto A estrutura em árvore dos arquivos do projeto de Coleta de Dados deve ser:

pico/
├── accelerometer_data_capture/
│   ├── accel_mma7361l.c
│   ├── CMakeLists.txt  
│   ├── pico_sdk_import.cmake
│   └── build/
│
├── pico-sdk/
│   ├── cmake
│   └── external
│   └── ...

Abaixo do código-fonte para coleta de dados:

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"
#include "pico/binary_info.h"
 
#define NSAMP 10
 
#define G0 1.65f
#define CONVERT_G_TO_MS2    9.80665f
#define FREQUENCY_HZ        50
#define INTERVAL_MS         (1000 / (FREQUENCY_HZ + 1))
 
const float conversion_factor = 3.3f / (1 << 12);
 
float get_axis (int adc_n) {
    adc_select_input(adc_n);
    unsigned int axis_raw = 0;
    for (int i=0;i<NSAMP;i++){
        axis_raw = axis_raw + adc_read();
        sleep_ms(1);
    }
    axis_raw = axis_raw/NSAMP;
    float axis_g = (axis_raw*conversion_factor)-G0;
    return axis_g;
}
 
int main() {
 
    stdio_init_all();
     
    adc_init();
 
    adc_gpio_init(26);
    adc_gpio_init(27);
    adc_gpio_init(28);
 
    while (1) {
 
        printf("%f \t", (get_axis (0) * CONVERT_G_TO_MS2));
        printf("%f \t", (get_axis (1) * CONVERT_G_TO_MS2));
        printf("%f \n", (get_axis (2) * CONVERT_G_TO_MS2));
         
        sleep_ms(INTERVAL_MS);
    }
}

O código acima lê os 3 ADCs (10 vezes cada, entregando um valor médio (valor suave). Os valores dos dados, um para cada eixo, separados por tabulações e convertidos em m/s, são enviados para a saída Pico USB usando a instrução printf(). A frequência de leitura foi definida em 50 Hz, mas deve ser menor que isso devido ao tempo de captura de dados e processamento suave.

Abaixo do CMakeLists.txt. Observe a linha: pico_enable_stdio_usb (accel_ml 1), isso significa que o USB (Serial 0) está habilitado.

cmake_minimum_required(VERSION 3.13) 
 
include(pico_sdk_import.cmake)
 
project(accelerometer_mma7361l_project C CXX ASM) 
set(CMAKE_C_STANDARD 11) 
set(CMAKE_CXX_STANDARD 17) 
pico_sdk_init()
 
add_executable(accel_ml 
    accel_mma7361l.c
)
 
pico_enable_stdio_usb(accel_ml 1)
 
pico_add_extra_outputs(accel_ml) 
target_link_libraries(accel_ml pico_stdlib hardware_adc)

Depois de copiar pico_sdk_import.cmake para seu projeto de pasta, vá para a subpasta de compilação e repita o mesmo procedimento feito com o projeto blink:

cd build
export PICO_SDK_PATH=../../pico-sdk
cmake ..
make -j4

O código compilado final (accel_ml.uf2) aparecerá em uma pasta de construção.

Pressione Boot e Reset para abrir a janela RPI-RP2 e arraste o arquivo de projeto compilado accel_ml.uf2 para essa pasta, da mesma forma que fez com o piscar. O Pico vai começar a capturar dados dos acelerômetros e enviá-los para USB (Serial 0).

Agora você pode ler em um monitor serial.

Se você estiver usando Linux, uma boa opção é um minicom. Primeiro, instale-o:

sudo apt install minicom

E abra o monitor serial:

minicom -b 115200 -o -D /dev/ttyACM0

Uma alternativa é o programa Serial ou mesmo o Arduino IDE Serial Monitor and Plotter em um macOS.

O que precisamos fazer agora é coletar amostras de dados (conjunto de valores de 3 eixos) para cada um dos gestos feitos pelo homem que queremos classificar:

  • “Cima para baixo” (mover o Pico/Sensor de uma posição alta para uma posição inferior)
  • “Esquerda-direita” (Movendo o Pico/Sensor da esquerda para a direita e vice-versa)
  • “círculo”. (Movendo o Pico/Sensor em círculos CW e CCW).
  • “Descansando” (deixou o Pico/Sensor sobre a mesa, sem movimento)

Se você não possui uma conta no Edge Impulse Studio, faça-o agora! O Edge Impulse é a plataforma de desenvolvimento líder para aprendizado de máquina em dispositivos de ponta, gratuito para desenvolvedores e confiável para empresas. Abra uma conta e crie um novo projeto.

Meu projeto é público e pode ser clonado aqui: Pico_Motion_Detection.

Depois de criar seu projeto, instale o Edge Impulse CLI em seu computador; para isso, siga estas instruções: Instalação CLI. Isso deve ser feito apenas uma vez.

Uma vez que o projeto é criado e o CLI é instalado, a maneira mais fácil de obter dados do Pico é usando o encaminhador Edge Impulse Data. Isso permite encaminhar os dados coletados por meio de uma interface serial para o estúdio. Este método só funciona perfeitamente em sensores com frequências de amostragem mais baixas, como no nosso caso (gestos humanos).

O encaminhador de dados é usado para retransmitir facilmente os dados de qualquer dispositivo para o Edge Impulse via serial (exatamente o nosso caso). Os dispositivos gravam os valores do sensor em uma conexão serial e o encaminhador de dados coleta os dados, assina os dados e os envia ao serviço de ingestão.

Em seu terminal, execute:

edge-impulse-data-forwarder

A CLI pedirá suas credenciais, o nome do projeto que você está trabalhando e o nome dos valores dos dados que iremos capturar (observe que a CLI já analisou o serial e sabe que os dados do sensor de 3 eixos estão disponíveis) e finalmente, pedirá um nome de dispositivo (opcional).

Retorne ao Edge Impulse Studio e vá para a seção de aquisição de dados:

O nome do seu dispositivo, junto com o sensor disponível e a frequência de captura, devem aparecer automaticamente. Defina o rótulo de dados e a quantidade de amostra desejada (o padrão é 10s) e pressione Iniciar amostra. Abaixo, 10 segundos do gesto up_down.

Observe que accZ (linha azul) tem as fotos mais altas, o que faz sentido.

“Aprendizado de máquina é uma forma de escrever programas que processam dados brutos e os transformam em informações que são significativas em um nível de aplicativo”, portanto, quantos dados você tem, mais informações você pode obter! Vamos capturar pelo menos 60 segundos de dados para cada rótulo. Tente equilibrar seu conjunto de dados, tendo aproximadamente a mesma quantidade de dados para cada rótulo (classe).

 

Engenharia de Recursos

Agora temos todos os dados brutos que serão necessários para o treinamento. Mas, como você viu na última imagem, os dados brutos são um tipo de dados de série temporal e não é fácil criar um modelo que entenda esse tipo de dados. Portanto, os dados devem ser pré-processados. Para isso, tomaremos uma janela de 2 segundos e extrairemos alguns valores relevantes, por exemplo, o valor RMS para tal grupo de dados e seus principais componentes de frequência (FFT). De cada janela, 33 recursos serão gerados (11 por eixo).

Pode parecer complexo, mas a boa notícia é que o Edge Impulse fará isso quase que automaticamente para nós.

 

Definição de Engenharia e Modelo de Recursos (Projeto de Impulso)

Vamos voltar um pouco. Depois de obter os dados brutos, vá para a seção Impulse Design e crie o impulso do projeto. Um impulso pega dados brutos, usa processamento de sinal para extrair recursos e, em seguida, usa um bloco de aprendizagem para classificar novos dados.

Resumindo, o Impulse pegará os dados brutos, dividindo-os em segmentos de 2 segundos. Mas observe que essas janelas irão deslizar com o tempo, com 80ms de deslocamento. Com isso, mais dados serão gerados.

Na seção Spectral Features é possível definir os parâmetros gerais para a geração de recursos. Fiquei com os valores padrão, e na guia Gerar recursos foi possível explorar visualmente todas as 3.879 amostras geradas.

Nossas classes de conjunto de dados são muito bem definidas, o que sugere que nosso modelo classificatório deve funcionar bem.

Observe que os dados laranja (esquerda-direita) vão principalmente no eixo y, e os dados vermelhos (cima-baixo) vão ao longo do eixo z. Além disso, o repouso (ponto verde) não mostra nenhuma aceleração, o que é esperado (no estágio anterior, a aceleração da terra (g) foi filtrada do eixo z.

 

Projetar e treinar o classificador de redes neurais (NN)

O modelo do Classificador NN pode ser muito simples:

O modelo possui 33 neurônios em sua primeira camada (1 neurônio para cada uma das características) e 4 neurônios na última camada (1 neurônio para cada uma das 4 classes). O modelo possui 2 camadas ocultas, respectivamente, com 20 e 10 neurônios.

Os hiperparâmetros padrão são 30 épocas (isso é muito e podem ser reduzidos pela metade neste caso) e uma taxa de aprendizado de 0,0005. Executando o treinamento, finalizamos com uma precisão de 100%, confirmada pelo F1 Score. Isso não é normal com projetos de Deep Learning, mas pudemos perceber que as classes de dados foram muito bem divididas. Apenas 80% dos dados foram utilizados para treinamento durante a fase de treinamento, restando 20% para validação. Ambos os conjuntos de dados tiveram um bom desempenho e não parecem que o modelo foi super ajustado, conforme mostrado no gráfico de Perda vs. Época:

Aqui está o resultado do treinamento Edge Impulse Studio:

Espera-se que esse modelo quantizado leve em torno de 1ms no tempo de inferência, usando 1,5Kb em RAM e 15,4Kb em ROM. Muito bem!

 

Testando o modelo com dados reais (novos)

Na seção de classificação do Studio Live, você pode repetir o que foi feito durante a fase de captura de dados. Uma vez que mantive o mesmo tipo de movimentos, o resultado foi excelente.

Todos os dados capturados nesta seção são armazenados como Dados de Teste, analisados na Seção Aquisição de Dados, na guia Test Data.

A próxima fase testa o modelo com dados completamente novos (armazenados na seção Aquisição de dados/Dados de teste). O resultado foi excelente novamente, apenas com alguma confusão misturando left_right com o círculo, o que era esperado.

 

Conversão e implantação

Depois que o modelo é desenvolvido, treinado e testado, a próxima etapa em nosso fluxo de trabalho de aprendizado de máquina é conversão e implantação.

Na seção de implantação de impulso de borda é possível implantar o modelo treinado e o bloco de pré-processamento (processamento de sinal), como uma biblioteca C++.

Para MCUs que funcionam com Arduino IDE, o estúdio gera automaticamente as bibliotecas e exemplos viáveis que podem ser usados como ponto de partida para inferência real.

No caso do Raspberry Pi Pico, escolheremos a opção Biblioteca C++, uma vez que este MCU ainda não funciona com o Arduino IDE. Mas, ainda usaremos um dos exemplos de código do Arduino como nosso ponto de partida.

Além disso, vamos habilitar o compilador Edge Optimized Neural (EON ™), que permite a execução de redes neurais em 25-55% menos RAM e até 35% menos flash, mantendo a mesma precisão, em comparação com TensorFlow Lite para Microcontroladores , como podemos ver abaixo:

Pressionar o botão CONSTRUIR nesta seção de estúdio fará o download do pacote completo a ser usado em nosso projeto final.

 

Inferência

Agora é hora de realmente fazer o aprendizado de máquina em dispositivos de incorporação! Vamos programar o nosso Pico para reconhecer os gestos totalmente off-line, sem ligação à Internet. Esta é a revolução que o TinyML está fazendo!

Preparando o ambiente do projeto de Reconhecimento de Gestos

O pacote C/C++ baixado do Edge Impulse Studio tem as seguintes pastas/arquivos:

├── edge-impulse-sdk/
├── model-parameters/
├── tflite-model/
├── CMakeLists.txt

Vamos atualizar o CMakeLists.txt com as informações específicas necessárias para nosso projeto e adicionar nosso código-fonte C++ (que estará em uma pasta de código-fonte).

A estrutura em árvore dos arquivos do projeto Coleção de reconhecimento de gestos deve ser:

pico/
├── pico_gesture_recognition_inference/
│   ├── edge-impulse-sdk/
│   ├── model-parameters/
│   ├── tflite-model/
│   ├── source/
│   ├── CMakeLists.txt  
│   ├── pico_sdk_import.cmake
│   └── build/
│
├── pico-sdk/
│   ├── cmake
│   └── external
│   └── ...

Com base no ótimo tutorial Inferência de aprendizado de máquina no Raspberry Pico 2040 por Dmitry Maslov, que me inspirou neste projeto, poderíamos criar o CMakeLists.txt abaixo. O programa executável final será denominado “app”:

cmake_minimum_required(VERSION 3.13)
 
set(MODEL_FOLDER .)
set(EI_SDK_FOLDER edge-impulse-sdk)
 
include(pico_sdk_import.cmake)
 
project(pico_motion_detection_project C CXX ASM) 
set(CMAKE_C_STANDARD 11) 
set(CMAKE_CXX_STANDARD 17) 
pico_sdk_init()
 
add_executable(app 
    source/main.cpp
    source/ei_classifier_porting.cpp
)
 
include(${MODEL_FOLDER}/edge-impulse-sdk/cmake/utils.cmake)
 
pico_enable_stdio_usb(app 1)
  
target_link_libraries(app pico_stdlib hardware_adc)
 
add_subdirectory(${MODEL_FOLDER}/edge-impulse-sdk/cmake/zephyr)
 
target_include_directories(app PRIVATE
    ${MODEL_FOLDER}
    ${MODEL_FOLDER}/classifer
    ${MODEL_FOLDER}/tflite-model
    ${MODEL_FOLDER}/model-parameters
)
 
target_include_directories(app PRIVATE
    ${EI_SDK_FOLDER}
    ${EI_SDK_FOLDER}/third_party/ruy
    ${EI_SDK_FOLDER}/third_party/gemmlowp
    ${EI_SDK_FOLDER}/third_party/flatbuffers/include
    ${EI_SDK_FOLDER}/third_party
    ${EI_SDK_FOLDER}/tensorflow
    ${EI_SDK_FOLDER}/dsp
    ${EI_SDK_FOLDER}/classifier
    ${EI_SDK_FOLDER}/anomaly
    ${EI_SDK_FOLDER}/CMSIS/NN/Include
    ${EI_SDK_FOLDER}/CMSIS/DSP/PrivateInclude
    ${EI_SDK_FOLDER}/CMSIS/DSP/Include
    ${EI_SDK_FOLDER}/CMSIS/Core/Include
)
 
include_directories(${INCLUDES})
 
# find model source files
RECURSIVE_FIND_FILE(MODEL_FILES "${MODEL_FOLDER}/tflite-model" "*.cpp")
RECURSIVE_FIND_FILE(SOURCE_FILES "${EI_SDK_FOLDER}" "*.cpp")
RECURSIVE_FIND_FILE(CC_FILES "${EI_SDK_FOLDER}" "*.cc")
RECURSIVE_FIND_FILE(S_FILES "${EI_SDK_FOLDER}" "*.s")
RECURSIVE_FIND_FILE(C_FILES "${EI_SDK_FOLDER}" "*.c")
list(APPEND SOURCE_FILES ${S_FILES})
list(APPEND SOURCE_FILES ${C_FILES})
list(APPEND SOURCE_FILES ${CC_FILES})
list(APPEND SOURCE_FILES ${MODEL_FILES})
 
# add all sources to the project
target_sources(app PRIVATE ${SOURCE_FILES})
 
pico_add_extra_outputs(app)

Tomando como ponto de partida o exemplo do Arduino: nano_ble33_sense_accelerometer.ino e mudando as instruções que não são compatíveis, crie o arquivo main.cpp abaixo. O LED interno piscará durante o tempo em que os dados são capturados e classificados:

/* Includes ---------------------------------------------------------------- */
#include <stdio.h>
#include "pico/stdlib.h"
#include "ei_run_classifier.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"
 
/* Constant defines -------------------------------------------------------- */
#define CONVERT_G_TO_MS2    9.80665f
#define G0 1.65f
#define NSAMP 10
 
/* Private variables ------------------------------------------------------- */
static bool debug_nn = false; // Set this to true to see e.g. features generated from the raw signal
 
const float conversion_factor = 3.3f / (1 << 12);
 
const uint LED_PIN = 25;
 
float readAxisAccelation (int adc_n) {
    adc_select_input(adc_n);
    unsigned int axis_raw = 0;
    for (int i=0;i<NSAMP;i++){
        axis_raw = axis_raw + adc_read();
        sleep_ms(1);
    }
    axis_raw = axis_raw/NSAMP;
    float axis_g = (axis_raw*conversion_factor)-G0;
    return axis_g;
}
 
int main()
{
    stdio_init_all();
     
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    gpio_put(LED_PIN, 0);
     
    adc_init();
    adc_gpio_init(26);
    adc_gpio_init(27);
    adc_gpio_init(28);
     
    if (EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME != 3) {
        ei_printf("ERR: EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME should be equal to 3 (the 3 sensor axes)\n");
        return 1;
    }
     
    while (true){
         
        ei_printf("\nStarting inferencing in 2 seconds...\n");
        sleep_ms(2000);
        gpio_put(LED_PIN, 1);
        ei_printf("Sampling...\n");
 
        // Allocate a buffer here for the values we'll read from the IMU
        float buffer[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE] = { 0 };
 
        for (size_t ix = 0; ix < EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; ix += 3) {
            // Determine the next tick (and then sleep later)
            uint64_t next_tick = ei_read_timer_us() + (EI_CLASSIFIER_INTERVAL_MS * 1000);
             
            buffer[ix] = readAxisAccelation (0);
            buffer[ix + 1] = readAxisAccelation (1);
            buffer[ix + 2] = readAxisAccelation (2);
 
            buffer[ix + 0] *= CONVERT_G_TO_MS2;
            buffer[ix + 1] *= CONVERT_G_TO_MS2;
            buffer[ix + 2] *= CONVERT_G_TO_MS2;
 
            sleep_us(next_tick - ei_read_timer_us());
        }
 
        // Turn the raw buffer in a signal which we can the classify
        signal_t signal;
        int err = numpy::signal_from_buffer(buffer, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
        if (err != 0) {
            ei_printf("Failed to create signal from buffer (%d)\n", err);
            return 1;
        }
 
        // Run the classifier
        ei_impulse_result_t result = { 0 };
 
        err = run_classifier(&signal, &result, debug_nn);
        if (err != EI_IMPULSE_OK) {
            ei_printf("ERR: Failed to run classifier (%d)\n", err);
            return 1;
        }
 
        // print the predictions
        ei_printf("Predictions ");
        ei_printf("(DSP: %d ms., Classification: %d ms., Anomaly: %d ms.)",
            result.timing.dsp, result.timing.classification, result.timing.anomaly);
        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 EI_CLASSIFIER_HAS_ANOMALY == 1
        ei_printf("    anomaly score: %.3f\n", result.anomaly);
    #endif
    gpio_put(LED_PIN, 0);
    }
 
#if !defined(EI_CLASSIFIER_SENSOR) || EI_CLASSIFIER_SENSOR != EI_CLASSIFIER_SENSOR_ACCELEROMETER
#error "Invalid model for current sensor"
#endif
return 0;
}

Junto na pasta de origem, está o arquivo ei_classifier_porting.cpp, também adaptado por Dmitri, que mantive como ele. Neste ponto, tendo todos os arquivos relevantes em nossa pasta de projeto, vá para a subpasta build e repita o mesmo procedimento fez com todos os projetos anteriores para compilar o código executável final:

cd build
export PICO_SDK_PATH=../../pico-sdk
cmake ..
make -j4

O código compilado final (app.uf2) aparecerá em uma pasta de compilação. Pressione Boot e Reset para abrir a janela RPI-RP2 e arraste o arquivo de projeto compilado app.uf2 para essa pasta, da mesma forma que fez com outros projetos. O Pico começará a amostrar dados dos acelerômetros a cada 2 segundos e exibirá as previsões do Monitor Serial.

Observe que o tempo de classificação (inferência) é de 1 ms, igual ao previsto pelo Edge Impulse Studio.

 

Considerações Finais

A próxima etapa neste projeto seria detectar anomalias, o que também é simples de implementar com o Edge Impulse Studio.

Para aqueles que estão curiosos para aprender mais sobre TinyML, sugiro fortemente o curso gratuito Coursera: Introdução ao Aprendizado de Máquina Embutido | Edge Impulse. Aqui um vislumbre de um dos projetos que desenvolvi durante o curso, classificando modos de operação e anomalias em um Blender:

Você também pode clonar este projeto no Edge Impulse Studio: Blender – Detecção de movimento.

 

Conclusão

A ideia geral deste projeto era aprender a programar um Raspberry Pi Pico e fazer uma prova de conceito de que é possível realizar Aprendizado de Máquina com este MCU, ainda não oficialmente suportado por Edge Impulse e Arduino, o que espero que aconteça em breve porque isso aconteceria simplifica enormemente todo o processo de codificação para desenvolvedores não especialistas.

Vejo vocês no meu próximo projeto!

Obrigada