Computer vision / machine learning

Usando tensorflow e um raspberry pi para ver se você está de máscara!

Criando um sistema de deteção e alarme via whatsapp

Paulo Eduardo Sampaio
11 min readFeb 18, 2021

--

Quem é vivo sempre aparece, não é mesmo? Esses dias eu comecei a dar aulas no curso intensivo de data science da awari, o que pra mim é super divertido: sempre adorei ensinar então mais do que um trabalho, é uma grande diversão. Em uma das aulas, falo sobre o como é importante ter um portfólio de projetos para apresentar como complemento ao CV e como o Medium é uma plataforma que permite você fazer isso de maneira simples e blá blá blá. Aí olho aqui o último texto que publiquei, outubro 2019!

Último texto que publiquei!! Faz tempo!

Há mais de um ano! Não é como se eu não tivesse feito nada — comecei a publicar no meu próprio blog em inglês, mas o foco lá é outro. Enfim, me comprometi com os alunos que publicaria um texto também e cá estou pra discutir mais um projeto que fiz por diversão aqui por casa!

Só é permitida entrada de pessoas com máscara!

Nesse ano caótico, assim como eu você deve ter visto em todos os lugares avisos do tipo “obrigatório uso de máscara”.

Elevadores do meu prédio: quem não estiver de máscara toma um esculacho.
Uber pede pra você tirar uma foto

E eu concordo com os dois casos, todos temos que usar máscara mesmo, mas minha questão é: como o síndico do meu prédio sabe se as pessoas estão usando máscara sem ter que ficar assistindo o feed da câmera de segurança 24 horas? E como a Uber pode garantir que o motorista não tirou a foto e depois tirou a máscara? O que vamos fazer aqui é um sistema que, através de uma câmera, monitora se a pessoa está usando máscara ou não.

Testando o projeto. Telecaster de fundo apenas para ganhar likes.

E, caso não esteja, manda uma mensagem via WhatsApp lembrando educadamente que a pessoa deveria usar a máscara:

Dráuzio mandando áudio cantando “Usar a máscara salva!”

A idéia é essa, vamos lá!

Arquitetura do projeto:

O plano é:

Arquitetura do projeto
  • Um dispositivo simples que tenha acesso ao feed da câmera (vamos um usar um Raspberry Pi para o exemplo, mas se vc não pode ser a camera do seu computador mesmo).
  • Esse dispositivo envia cada frame para um API
  • O API tem 2 passos:
  1. Deteção de faces: Se não tiver rosto, não preciso detectar se tem ou não máscara
  2. Classificação de com ou sem máscara
  • API responde para o dispositivo
  • Se a pessoa não estiver de máscara, usa o twilio pra mandar uma alerta

Ok, são diversos passos, mas todos soam factíveis! Mãos à obra!

O código inteiro, link para modelos e datasets está aqui: https://github.com/pauloesampaio/mask_detector

Mãos à obra

Detecção de faces

Esta parte é fácil pois hoje em dia as bibliotecas já tem isso praticamente de fábrica! Vamos usar os modelos de deep learning do OpenCV. A documentação desses modelos ainda está um pouco misteriosa mas eu ajudo vcs:

  • Baixe o arquivo de modelo_treinado e a model_architecture (ambos mantidos pelo próprio OpenCV)
  • Carregue o modelo: model = cv2.dnn.readNetFromTensorflow(modelo_treinado, arquitetura_do_model)
  • Como eu sou legal, fiz essa funçãozinha pra facilitar o uso:

Pra usar essa função você manda o modelo (que você carregou no passo 2) e uma imagem qualquer e ela vai te devolver uma lista com as coordenadas de onde está o rosto que ela encontrou, ou uma lista vazia caso não tenha um rosto na imagem.

Testando o detector de faces. Cabelo pré-pandemia. Cachorrinho para ganhar likes.

Pronto.

Classificação de com máscara ou sem máscara

Eu entrei no glorioso Kaggle e procurei “Mask detection dataset”. Achei alguns, juntei os que achei que faziam sentido e acabei com:

  • 4000 fotos com máscara
  • 4000 fotos sem máscara

A maioria dessas fotos são “in the wild” como diz a juventude: fotos em cenários e situações reais, o que é ótimo para a generalização do modelo. Os links para baixar esse dataset ou o modelo já treinado estão no repositório do projeto no meu github.

Como eu acho que este vai ser um problema relativamente simples para um modelo resolver e eu quero que a previsão rode rápido mesmo numa CPU, optei por utilizar a arquitetura chamada de MobileNetV2, disponível no tensorflow.

Treinar também não tá muito difícil não. Pessoal do Keras — já a algum tempo parte do tensorflow — fez um excelente trabalho em facilitar esta parte. E claro, se você não tem um computador gamer cheio de led e uma GPU boladona, recomendo treinar no google colab usando a GPU deles (para ativar a GPU, menu runtime -> change runtime -> hardware acceleration: GPU).

O arquivo de treinamento esta inteiro aqui, mas vou comentar os pontos principais:

Aqui estamos usando o “functional api” do keras para construir um modelo.

  • Começamos com o input shape e definimos um layer de input
  • Aplicamos a função de pre-processing a esse input. Precisa? Acho questionável e adoraria conversar sobre isso. Acho que tem tantos pontos a favor como contra e seria uma discussão muito longa para colocar aqui. Eu costumo adicionar se as imagens com que estou trabalhando são no mesmo estilo das imagens do Imagenet, origem dessa função de pre-processamento. Se forem muito diferentes (imagens médicas, de satélite, microscópicas, etc) não utilizo o preprocessing.
  • Agora sim definimos o “core” do nosso modelo, a rede MobileNetV2
  1. include_top=False pois não queremos usar o layer de classificação padrão dela (as 1000 classes do Imagenet)
  2. weights='imagenet' pois embora não queiramos usar o layer de classificação, vamos utilizar a rede pré treinada com a base de dados do imagenet. Esse é o famoso transfer learning, pegamos uma rede treinada para uma atividade e re-treinamos ela para a nossa atividade específica.
  3. pooling='avg' é só uma indicação do que fazer para transformar o último layer em um vetor.
  • Definimos o output com duas unidades (afinal temos duas classes — com e sem máscara) e ativação softmax, que nos dá probabilidades como saída. Poderia usar apenas 1 unidade e ativação sigmoid e tornar um problema de classificação binária ao invés de multi-classe? Poderia. Deixei como multi-classe pois é mais genérico e vc pode reutilizar se tiver mais classes. Viu como eu sou seu amigo?

Temos nosso modelo. Maravilha! Agora como passamos pelos nossos dados? Tensorflow também tem uma classe que torna isso mais fácil, ImageDataGenerator. Vejamos:

Aqui começamos com o ImageDataGenerator, que recebe as instruções do que fazer com as imagens:

  • Ele vai gerar aleatoriamente versões das imagens com algumas das transformações indicadas (rotação, zoom, shift, brightness, flip, etc…). Uma lista das transformações possíveis está na documentação. Pra que isso? Bom, algoritmos de deep learning se favorecem por quanto mais dados disponíveis. Então adicionando variações à imagem você, de forma simples e criativa, está aumentando a quantidade de dados disponíveis. É o que se chama “data augmentation”.
  • validation_split=0.2 define como vai ser a separação de treino e teste. Aqui por exemplo estou treinando com 80% dos dados e deixando 20% para teste.

Tendo isso definido, a classe tem diversas maneiras de como abrir seus dados e fazer algo com eles. Eu particularmente gosto da flow_from_dataframe, que estou usando neste exemplo. Está explicadinho na documentação, mas basicamente ela recebe:

  • Um dataframe de pandas listando todos os arquivos de imagens que você tem na sua base de dados e com colunas para o path e label dos arquivos
  • O parametro x_col recebe o nome da coluna do path e o y_col o nome da coluna da label.
  • target_size é o tamanho de imagem que seu modelo espera. Quando abrir as imagens, ele já vai redimensionar para esse tamanho.
  • subset, para saber se os dados gerados aqui vão ser o de teste ou treino. Neste caso, coloquei como exemplo o de treino. Para o de teste, faça um test_generator exatamente igual mas com subset="validation"

Pronto, agora é só treinar com o comando:

Repare que estou simplesmente chamando o fit do modelo com o gerador de treino, validando no gerador de teste, por 100 ciclos e usando 3 callbacks:

  • LR_reducer: Se o modelo para de aprender, ele reduz a learning_rate
  • early_stopping: Se mesmo reduzindo a learning rate o modelo continua não aprendendo mais nada, para o treinamento
  • csv_logger: Salva a saída do treinamento para um arquivo csv.

Ufa. Foi bastante coisa mas nada fora do comum, certo? Pra aprender um pouco mais sobre como esse treinamento funciona, meu texto anterior explica com um exemplo mais simples.

Ao final temos um modelo com uma bela matriz de confusão:

Confusion matrix

Falei que era um problema relativamente fácil para uma convolutional neural network!

API de classificação

Modelo criado, hora de escrever um API para que possamos enviar imagens e ter como resposta se a pessoa está com máscara ou não. Para isso vamos usar o FastAPI, maneira mais rápida e simples de se criar APIs. Para criar um endpoint de previsão, basta fazer:

Repare que:

  • Eu estou recebendo uma imagem em formato string e passando ela pro formato de imagem usando a função load_string_to_image
  • Estou passando a imagem para a função find_faces, que a gente já viu que devolve uma lista com as coordenadas das faces encontrados
  • Se o tamanho da lista for maior que zero (ou seja, se existe um rosto na foto), passo a imagem para nosso modelo de classificação de máscara
  • Aqui estou usando o orjson só para converter o resultado da previsão de float32 para algo que pode ser serializado via json... por default, json não lida bem com float32
  • Crio a resposta do nosso API com a previsão e o timestamp.

Pronto. Como foi fácil demais, vamos adicionar uma sofisticação ao fazer esse API rodar em um container pelo menos! Rodar em container facilita muito na hora de colocar em produção e se for necessário escalar. O pessoal do FastAPI já fornece uma Docker image o que torna tudo mais fácil. De qualquer forma, coloquei aqui o Dockerfile e o docker-compose.yml que utilizei.

Enviando mensagens de whatsapp via twilio

O twilio é uma plataforma de comunicação que permite via python (ou qualquer outra linguagem) enviar mensagens de SMS, fazer ligações e, no nosso caso, usar o WhatsApp. Ele é pago, mas o free trial dele é mais que suficiente pra vc testar e se divertir. Se inscreva, siga todos os passos e quando estiver pronto, para mandar mensagem via WhatsApp:

Aqui não tem muito segredo:

  • Inicie o client com seus dados do twilio (que você consegue ao fazer a inscrição no free trial)
  • from_: É o seu número do twilio
  • to: É o número para onde vc quer mandar mensagem
  • body: É a mensagem que você quer enviar.

Pronto, resolvido.

Coletando frames de vídeo com o Raspberry Pi

Acho que antes vale um “O que é um Raspberry Pi”. Se você não conhece ou apenas conhece como emulador de video-game retrô, o Raspberry Pi é um computadorzinho completo em uma placa do tamanho de um cartão de crédito e com um preço (fora do Brasil) bastante acessível.

Raspberry Pi dentro do case e camera. Cachorrinho desfocado ao fundo para ganhar likes.

Um novo, modelo 4, custa 35 dólares nos EUA. Infelizmente aqui no Brasil a gente sabe que o pessoal dá aquela inflacionada, então o mais barato que eu consegui achar foi um modelo 3 por R$ 270… mas enfim… O que estou usando é um bem antigo, modelo 2, que comprei em 2014 ou 2015, mas ainda funciona bem. Digo isso só para mostrar que você não precisa comprar o mais recente. Se encontrar um usado barato, manda ver. É comumente utilizado para fins educacionais, experimentos de IoT e protótipos em geral. Ele tem uma câmera opcional, mas você pode ligar uma câmera USB que funciona também.

Se você não tiver um, tudo bem, dá pra brincar com a webcam do seu computador mesmo!

Eu vou usar aqui a biblioteca imutils, escrita pelo glorioso Adrian Rosebrock, do excelente pyimagesearch — qualquer dia conto da minha experiência com o curso de computer vision deles. A vantagem dessa biblioteca é que ela usa multithreading (basicamente usando o máximo possível de poder computacional disponível) para acessar a câmera, deixando o framerate mais ligeiro.

Para acessar a câmera com o imutils, este é o código:

Se você colocar usePiCamera=False, pode usar com a câmera do seu computador mesmo!

Repare que ali no meio coloquei “Your code to deal with frames here”. É o que vamos ver na sequência!

Juntando os pedaços

Ok, então nós temos:

  • Uma câmera nos fornecendo um stream de vídeo, que no fim das contas é uma sequencia de imagens (frames).
  • Um API rodando nosso modelo que detecta faces e se está de máscara ou não
  • Uma função que manda mensagens

Hora de conectar essas pontas! Naquela pedaço onde eu tinha escrito Code to deal with frames, isso é o que estou fazendo:

Passo a passo:

  • Capturamos o frame
  • Transformamos a imagem em uma base64 string para mandar para o API (é assim que APIs conversam…)
  • Da resposta do API pegamos a probabilidade da pessoa estar de máscara:
  1. Se a probabilidade for nula (None), sinal que não tinha um rosto na imagem. Definimos uma cor e uma label apenas para exibir na tela.
  2. Se a probabilidade for acima de um gatilho (estou usando .5) a pessoa está de máscara. Definimos uma cor e uma label para exibir na tela
  3. Se a probabilidade for abaixo de um gatilho (estou usando .5) a pessoa está sem máscara. Definimos uma cor e uma label para exibir na tela. E precisamos enviar uma mensagem!
  • Se o tempo desde a última mensagem for superior a um gatilho (eu estou usando 30 minutos só para teste), envia uma mensagem usando o twilio e anota o timestamp da mensagem.
  • Desenha um quadrado e exibe a mensagem definida no frame.

Pronto, é isso! Repare que em vários lugares aparecem config e credentials. Eu tenho o hábito de criar um arquivo de configuração e um de credenciais para não deixar nada hard-coded, nenhuma senha exposta e facilitar mudanças de configurações. Por exemplo, meu arquivo de config tem coisas como:

running_on_pi: True # Falso se você estiver rodando no seu computador ao invés do Raspberry Piview_frames: True # Falso se você estiver em produção e não precisar ver os frames geradosapi_url: http://192.168.15.10:8000/predict/ # Endereço de rede onde o API está rodandomessage_delta_time_minutes: 30 # Minutos entre as mensagens do twilio. Pra vc não inundar a conta de alguém por acidamente!

E meu arquivo de credentials tem o seguinte, bem auto explicativo:

"account_sid": "TWILIO SID",
"auth_token": "TWILIO AUTH TOKEN",
"from_number": "YOUR TWILIO NUMBER",
"to_number": "NUMBER YOU WANT TO SEND MESSAGES TO"

Conclusão

Este não é um projeto fácil, eu sei: tem diversas partes, vários detalhes, usa hardware externo, comunicação com API e tudo mais. Porém, quebrando em partes pequenas, todas elas fazem sentindo, não é mesmo? E é útil, já que vários negócios precisam garantir que os protocolos estão sendo seguidos! E divertido!

Eu gosto de computer vision exatamente pelas coisas serem extremamente visuais, fica fácil entender o que está se passando. Mais uma vez, o código está inteiro disponível aqui: https://github.com/pauloesampaio/mask_detector e qualquer dúvida ou comentário eu sempre fico feliz em ler e interagir!

Espero que tenham gostado, que tenham aprendido alguma coisa e comentários são sempre bem vindos!

Abraços!

--

--

Paulo Eduardo Sampaio

Cientista de dados, engenheiro, músico de fim de semana e fã dos piores programas da TV aberta