A visão computacional é fantástica. Com este recurso, um sistema computacional pode "aprender a enxergar" e, com isso, fazer tarefas cada vez mais complexas e úteis ao dia a dia moderno, tais como: identificar objetos, identificar pessoas e/ou faces, reconhecer objetos e obter características deles, determinar movimento de objetos, mensurar velocidade de objetos, e por aí vai. E o sistema computacional em questão pode ser uma Single-Board Computer comum, como uma Raspberry Pi, por exemplo.

 

Neste artigo será mostrado um uso da Raspberry Pi em visão computacional: com base no OpenCV e Python, permitir contagem de objetos em movimento utilizando visão computacional e envio periódico desta contagem para a plataforma IoT ThingSpeak, de modo a permitir um acompanhamento preciso de qualquer lugar do mundo.

 

Materiais necessários

Para fazer este projeto, serão necessários os seguintes materiais:

Uma Raspberry Pi 3B (com cartão de tamanho recomendado de 16GB);

Uma webcam USB comum*;

Fonte de alimentação para Raspberry Pi (recomendado: 5V/3A).

 

* Pode ser qualquer modelo USB, desde que seja compatível com o Linux rodando na Raspberry Pi.

 

 

Montagem do projeto (hardware)

Assumindo que você já tem controle sobre a Raspberry Pi 3B (seja por acesso via VNC, SSH ou acesso local por um teclado e mouse conectados na placa ), a montagem do hardware do projeto é muito simples: basta ligar a webcam USB a uma das portas USB da Raspberry Pi 3B e ligar a fonte de alimentação na Raspberry Pi 3B.

Ainda, caso desejar utilizar conexão a Internet de forma cabeada, basta ligar o cabo de rede (RJ45) na entrada Ethernet da Raspberry Pi 3B.

 

Preparação: Instalação do OpenCV na Raspberry Pi

Antes de prosseguir com o projeto, é necessário instalar o OpenCV (biblioteca / framework para visão computacional) na Raspberry Pi 3B. No caso, isto envolverá compilar o OpenCV na Raspberry (processo um tanto quanto lento, mas que garante máximo desempenho deste no hardware do projeto).

 

Para isso, recomendo seguir as instruções deste artigo

 

 

Visão geral do projeto

Em termos de visão computacional, o projeto aqui mostrado se trata de um contador de objetos em movimento, com contagens independentes para objetos saindo e entrando na zona monitorada. Zona monitorada é o nome que se dá a imagem obtida pela webcam, ou seja, tudo que a Raspberry Pi 3B pode “ver” com a webcam é chamado de zona monitorada. Observe a figura 1.

 

 

Figura 1 -zona monitorada (entrada e saída) e suas definições
Figura 1 -zona monitorada (entrada e saída) e suas definições

 

 

Procedimento de contabilização de objetos em movimento

O incremento da contagem de objetos em movimento se dá quando o centroide do objeto (cuja definição será explicada logo mais) cruza uma das linhas de referência. Portanto, não importa o tamanho do objeto e tão pouco sua forma, já que será considerado apenas seu centroide como ponto identificador do objeto em movimento.

 

A contabilização de objetos é feita com base em três características:

Somente objetos em movimento serão detectados e considerados;

A contabilização ocorre somente se o objeto cruzar uma das linhas de referência;

A direção do movimento é relevante.

 

Tais características combinadas permitem contabilizar não só o número bruto de objetos que passaram pelas linhas de referência, mas também quantos objetos entraram e saíram da zona monitorada.

 

Figura 2 - Detecção de direção do movimento de um objeto com base nas linhas de referência
Figura 2 - Detecção de direção do movimento de um objeto com base nas linhas de referência

 

 

A contabilização de objetos em movimento será feita seguindo o procedimento descrito abaixo:

  • Contabilização de objetos que entraram na zona monitorada: tudo que cruza a linha azul, vindo a partir da linha vermelha (ou seja, está entre as linhas de referência, mas cruza a linha azul), será contabilizado como entrada da zona de monitoramento. Esta situação está evidenciada na Figura 2.a.
  • Contabilização de objetos que saíram na zona monitorada: analogamente, qualquer objeto que cruza a linha vermelha, vindo da linha azul (ou seja, está entre as linhas de referência, mas cruza a linha vermelha), será contabilizado como saída da zona de monitoramento. Esta situação está evidenciada na Figura 2.b.

 

Importante: a captura de frames de imagem da câmera e processamento de imagem de cada frame logo em seguida faz com que a captura de frames não seja exatamente em tempo real. Isso significa que alguns quadros podem ser ignorados / perdidos. Isso leva a um fato desfavorável ao projeto: há chances de que o exato quadro do centroide do objeto cruzando uma das linhas de referência da zona monitorada seja perdido, afetando assim a contagem e funcionamento do projeto como um todo. Para contornar ou minimizar este problema, foi adotada uma solução aqui chamada de tolerância de cruzamento das linhas de referência.

 

A tolerância de cruzamento das linhas de referência consiste em uma faixa estreita (de 2 pixels), para cima e para baixo de cada uma das linhas de referência da zona monitorada. Se o centróide do objeto em movimento estiver nessa zona de tolerância de uma das linhas, é considerado que este está cruzando esta linha de referência em questão. Observe a figura 3.

 

 

Figura 3 - zona de tolerância (onde é considerado que o centroide está cruzando uma das linhas de referência
Figura 3 - zona de tolerância (onde é considerado que o centroide está cruzando uma das linhas de referência)

 

 

Processo / algoritmo do projeto para a detecção de objetos em movimento

Conforme explicado anteriormente neste artigo, é possível detectar que um objeto cruzou uma das linhas de referência e, além disso, saber sua direção de movimento (entrando ou saindo da zona monitorada).

Agora, veremos como detectar um objeto em movimento. As etapas a seguir são executadas no processamento/tratamento de imagens na exata ordem em que são aqui apresentadas.

 

Etapa 1 - realce do objeto em movimento

Na física, para classificar se um objeto qualquer está em movimento ou não, deve-se adotar uma referência. Aqui o princípio é exatamente o mesmo: para saber se um objeto está se movendo, compara-se um frame (imagem da webcam) capturado recentemente com um frame-referência. Esta comparação consiste em um recurso de visão computacional chamado na literatura técnica desubtração de background . Este recurso consiste em eliminar o máximo possível de informações irrelevantes da imagem, como o fundo dela (que, em teoria, se manterá o mesmo sempre), por exemplo (daí o "background" do nome da técnica), e realçar características desejadas.

Cada aplicação / projeto tem um método adequado de subtração de background, pois cada aplicação vai exigir que uma determinada característica da imagem seja realçada.

No caso deste projeto, a subtração de background é feita da forma mais intuitiva e direta possível: uma imagem em escala de cinza é, basicamente, um grande array bidimensional. Portanto, quaisquer operações matemáticas feitas pixel-a-pixel podem ser feitas com a imagem. Considerando isso, dois frames (um capturado recentemente e um frame-referência) são convertidos para escala de cinza, sofrem ação do filtro Gaussian Blur (para suavizar contornos, o que significa na prática deixar objetos mais "uniformes" / menos detalhados nas etapas subsequentes de tratamento/processamento de imagem) e subtraídos (ponto-a-ponto / pixel-a-pixel deste array bidimensional), de modo quesomente o que variou de um frame para outro seja realçado .

Isso acontece pois, como objetos em movimento vão, obrigatoriamente, causar variações em relação ao frame-referência, estes serão realçados pela subtração de background pois são justamente o que mudou de um frame para o outro. Sendo assim, após a subtração do background, idealmente ficará em destaque somente o objeto em movimento. É importante ressaltar aqui que, na prática, devido às características de iluminação e particularidades da webcam na captura de frames, dificilmente se terá dois frames que, subtraídos, sejam perfeitamente e somente o objeto em movimento. Há uma forma de contornar isso, algo que será visto mais à frente neste artigo.

Como exemplo, um frame com o background subtraído e seu movimento realçado em cores mais claras na escala de cinza pode ser visto na figura 4.

 

 

Figura 4 - frame com background subtraído e objeto em movimento realçado por cores mais claras
Figura 4 - frame com background subtraído e objeto em movimento realçado por cores mais claras

 

 

Etapa 2 - binarização

Em visão computacional, é comum que após realçar as características desejadas a imagem passe por um processo de binarização. Este processo consiste em transformar a imagem na escala de cinza em uma que tenhasomente duas cores: preto e branco . Isso é feito comparando as cores de cada pixel a um valor de referência / limiar (chamado na literatura técnica de threshold): se o valor da cor do pixel for abaixo do threshold, o pixel ganha a cor preta. Caso contrário, ganha a cor branca.

Isso é feito pois, matematicamente e computacionalmente, é muito mais simples se trabalhar com imagens binarizadas (de apenas 2 tipos possíveis de cor, preto e branco), o que leva a uma redução drástica de demanda de tempo de processamento do hardware que executa o projeto/análise. Esta técnica será aplicada neste projeto também.

O único ponto de atenção na técnica de binarização é o valor escolhido de threshold. Infelizmente, este é dependente da iluminação do local. Portanto, muito provavelmente este valor deve ser ajustado empiricamente, variando de caso para caso. Na figura 5, pode-se ver a figura 4 após a binarização.

 

Figura 5 - imagem após binarização com threshold adequado à iluminação do local
Figura 5 - imagem após binarização com threshold adequado à iluminação do local

 

 

Etapa 3 - dilatação

Os objetos em movimento até aqui detectados, suavizados e binarizados já estão quase na sua forma ideal de se trabalhar. O "quase" existe na frase pois há a possibilidade de existirem "buracos" no meio de objetos (ou seja, objetos que não são uma "massa" de cor única). Se tais buracos existirem no objeto, estes irão seguramente prejudicar as etapas posteriores de processamento/tratamento de imagem. Isso ocorre pois, em uma imagem com buracos, podem ser detectados mais de um objeto em uma imagem que se refere a um objeto apenas, ou seja, “falsos objetos” podem ser detectados. Isso afetaria diretamente a contagem final de objetos em movimento, arruinando o projeto aqui mostrado.

Para eliminar esta possibilidade de erro, é feito o processo de dilatação . Neste processo, o efeito é como se os pixels ao redor de buracos “crescessem” (na verdade, pixels ao redor seriam pintados com a cor do objeto após a binarização), de forma que o buraco tendesse a deixar de existir. No final deste processo, os objetos serão uma "massa" única de pixels de uma só cor.

 

Etapa 4 - procura por contornos (e seus centroides)

Neste ponto, já temos o objeto em movimento realçado e dilatados (sem "buracos"), de forma que o objeto em movimento foi transformado em uma “massa” de pixels de uma só cor. Essa "massa" de pixels é, na visão computacional, chamada de contorno.

Agora, o próximo passo é a detectar os contornos da imagem (= objetos em movimento) considerando suas áreas (em pixels²).

Lembra que na etapa de realce do objeto em movimento havia o problema de a subtração do background deixar resíduos (não ser somente e perfeitamente o objeto em movimento)? Isso é resolvido aqui, adotando-se uma área mínima (em pixels²) para algo ser considerado um objeto. Dessa forma, selecionam-se as maiores e mais relevantes áreas, que são os objetos em movimento propriamente ditos.

Desta detecção, é feita a obtenção das coordenadas e dimensões de retângulos que abrangem os objetos. Uma vez em posse destes dados, o centro de cada retângulo será equivalente aos chamados centroide do objetos em movimento. Portanto, temos aqui uma grande vantagem em termos computacionais: todo o movimento do objeto, por mais complexo que seja tal objeto, pode ser analisado pelo movimento de seu centroide apenas. O movimento do centroide do objeto representa o movimento do objeto todo.

Outra grande vantagem disso é que, por considerarmos apenas o centroide do objeto na análise de seu movimento, a forma e tamanho do objeto não serão relevantes no algoritmo de contagem em si, deixando o problema muito mais simples de ser resolvido.

Na figura 6, é possível ver na cor preta o centroide do objeto em movimento e, na cor verde, o retângulo que envolve o contorno do objeto detectado.

 

 

Figura 6 - frame colorido, com destaque para centroide do objeto em movimento (em preto) e do retângulo que envolve o contorno do objeto detectado (em verde)
Figura 6 - frame colorido, com destaque para centroide do objeto em movimento (em preto) e do retângulo que envolve o contorno do objeto detectado (em verde)

 

  

Etapa 5 - análise da posição do centroide de cada objeto em relação às linhas e referência

Uma vez analisando a trajetória dos objetos em movimento através dos movimentos de seus centroides, basta comparar a coordenada Y de cada centroide com as coordenadas Y das linhas de referência e, com base nisso, aplicar o algoritmo explicado no tópico Procedimento de contabilização de objetos em movimento deste artigo. Desta forma, contabiliza-se exatamente quais objetos em movimento entraram e saíram da zona monitorada.

Com isso, chega-se ao fim do processamento de imagens do projeto.

 

Parte IoT do projeto - monitoramento da contagem de objetos via ThingSpeak

Agora, chegamos a parte onde o projeto é inserido no contexto de Internet das Coisas (IoT). Este projeto contempla também a funcionalidade de envio periódico (de 15 em 15 segundos) da contagem de objetos em movimento para a plataforma IoT ThingSpeak, de forma a permitir um acompanhamento preciso de qualquer lugar do mundo.

Para usufruir dessa funcionalidade, você precisará se cadastrar na plataforma, criar um canal e obter a chave de escrita do canal criado. Para isso, siga o procedimento abaixo:

 

1. Acesse o site da plataforma ThingSpeak ( https://thingspeak.com/ )

2. No canto superior direito da tela, clique em Sign Up e faça seu cadastro.

Atenção: utilize um endereço de e-mail válido no seu cadastro. Após o cadastro, será necessário confirmá-lo a partir de uma mensagem de e-mail enviada pela ThingSpeak.

3. Uma vez confirmado seu cadastro, volte a acessar o site da plataforma ThingSpeak (https://thingspeak.com/) e faça seu login clicando em Sign In (canto superior direito da tela).

4. Na parte superior da página, clique em Channels e depois no botão New Channel

5. Preencha as informações conforme mostra a figura 7.

 

 

Figura 7 - dados para cadastro do canal na plataforma ThingSpeak
Figura 7 - dados para cadastro do canal na plataforma ThingSpeak

 

 

6. Clique em Save Channel

7. A tela de gerenciamento de seu canal vai surgir. Nela, clique sobre a aba API Keys e depois copie e salve em local seguro o conteúdo do campo Key em Write API Key (em destaque na figura 8). Esta informação será necessária mais a frente neste projeto.

 

Figura 8 - Write API Key do canal no ThingSpeak
Figura 8 - Write API Key do canal no ThingSpeak

 

 

 

Código-fonte do projeto

Abaixo encontra-se o código-fonte do projeto.

Importante 1 - leia todos os comentários para total entendimento do mesmo.

Importante 2 - não se esqueça de substituir a chave de escrita (Write API Key) de seu canal do ThingSpeak no local indicado no código-fonte (variável write_api_key_thingspeak).

 

import datetime
import math
import cv2
import numpy as np
import http.client, urllib.parse
import time
#variaveis globais
write_api_key_thingspeak = ''  #coloque aqui a Write API Key de seu canal ThingSpeak
width = 0
height = 0
contador_entradas = 0
contador_saidas = 0
area_contorno_limite_minimo = 3000  #este valor eh empirico. Ajuste-o conforme sua necessidade 
threshold_binarizacao = 70  #este valor eh empirico, Ajuste-o conforme sua necessidade
offset_linhas_referencia = 150  #este valor eh empirico. Ajuste-o conforme sua necessidade.
total_objetos_contados = 0   #variavel que contem o total de objetos em movimento contados (entrando ou saindo da zona monitorada)
#Verifica se o corpo detectado esta entrando da sona monitorada
def testa_interseccao_entrada(y, coordenada_y_linha_entrada, coordenada_y_linha_saida):
    diferenca_absoluta = abs(y - coordenada_y_linha_entrada)	
    if ((diferenca_absoluta <= 2) and (y < coordenada_y_linha_saida)):
        return 1
    else:
        return 0
#Verifica se o corpo detectado esta saindo da sona monitorada
def testa_interseccao_saida(y, coordenada_y_linha_entrada, coordenada_y_linha_saida):
    diferenca_absoluta = abs(y - coordenada_y_linha_saida)	
    if ((diferenca_absoluta <= 2) and (y > coordenada_y_linha_entrada)):
        return 1
    else:
        return 0
camera = cv2.VideoCapture(0)
#forca a camera a ter resolucao 640x480
camera.set(3,640)
camera.set(4,480)
primeiro_frame = None
#faz algumas leituras de frames antes de consierar a analise
#motivo: algumas camera podem demorar mais para se "acosumar a luminosidade" quando ligam, capturando frames consecutivos com muita variacao de luminosidade. Para nao levar este efeito ao processamento de imagem, capturas sucessivas sao feitas fora do processamento da imagem, dando tempo para a camera "se acostumar" a luminosidade do ambiente
for i in range(0,20):
    (grabbed, Frame) = camera.read()
timestamp_envio_thingspeak = int(time.time())
while True:
    #le primeiro frame e determina resolucao da imagem
    (grabbed, Frame) = camera.read()
    height = np.size(Frame,0)
    width = np.size(Frame,1)
    #se nao foi possivel obter frame, nada mais deve ser feito
    if not grabbed:
        break
    #converte frame para escala de cinza e aplica efeito blur (para realcar os contornos)
    frame_gray = cv2.cvtColor(Frame, cv2.COLOR_BGR2GRAY)
    frame_gray = cv2.GaussianBlur(frame_gray, (21, 21), 0)
    #como a comparacao eh feita entre duas imagens subsequentes, se o primeiro frame eh nulo (ou seja, primeira "passada" no loop), este eh inicializado
    if primeiro_frame is None:
        primeiro_frame = frame_gray
        continue
    #ontem diferenca absoluta entre frame inicial e frame atual (subtracao de background)
    #alem disso, faz a binarizacao do frame com background subtraido 
    frame_delta = cv2.absdiff(primeiro_frame, frame_gray)
    frame_threshold = cv2.threshold(frame_delta, threshold_binarizacao, 255, cv2.THRESH_BINARY)[1]
    
    #faz a dilatacao do frame binarizado, com finalidade de elimunar "buracos" / zonas brancas dentro de contornos detectados. 
    #Dessa forma, objetos detectados serao considerados uma "massa" de cor preta 
    #Alem disso, encontra os contornos apos dilatacao.
    frame_threshold = cv2.dilate(frame_threshold, None, iterations=2)
    
    #Abaixo estao as duas chamadas de cv2.findContours possiveis. 
    #Utilize aquela que funcionar com sua versão de OpenCV
    #_, cnts, _ = cv2.findContours(frame_threshold.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts, _ = cv2.findContours(frame_threshold.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    qtde_contornos = 0
    #desenha linhas de referencia 
    coordenada_y_linha_entrada = int((height / 2)-offset_linhas_referencia)
    coordenada_y_linha_saida = int((height / 2)+offset_linhas_referencia)
    cv2.line(Frame, (0,coordenada_y_linha_entrada), (width,coordenada_y_linha_entrada), (255, 0, 0), 2)
    cv2.line(Frame, (0,coordenada_y_linha_saida), (width,coordenada_y_linha_saida), (0, 0, 255), 2)
    #Varre todos os contornos encontrados
    for c in cnts:
        #contornos de area muto pequena sao ignorados.
        if cv2.contourArea(c) < area_contorno_limite_minimo:
            continue
        #Para fins de depuracao, contabiliza numero de contornos encontrados
        qtde_contornos = qtde_contornos+1    
        #obtem coordenadas do contorno (na verdade, de um retangulo que consegue abrangir todo ocontorno) e
        #realca o contorno com um retangulo.
        (x, y, w, h) = cv2.boundingRect(c) #x e y: coordenadas do vertice superior esquerdo
                                           #w e h: respectivamente largura e altura do retangulo
        cv2.rectangle(Frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
        #determina o ponto central do contorno e desenha um circulo para indicar
        coordenada_x_centroide_contorno = int((x+x+w)/2)
        coordenada_y_centroide_contorno = int((y+y+h)/2)
        ponto_central_contorno = (coordenada_x_centroide_contorno,coordenada_y_centroide_contorno)
        cv2.circle(Frame, ponto_central_contorno, 1, (0, 0, 0), 5)
        
        #testa interseccao dos centros dos contornos com as linhas de referencia
        #dessa forma, contabiliza-se quais contornos cruzaram quais linhas (num determinado sentido)
        if (testa_interseccao_entrada(coordenada_y_centroide_contorno,coordenada_y_linha_entrada,coordenada_y_linha_saida)):
            contador_entradas += 1
        if (testa_interseccao_saida(coordenada_y_centroide_contorno,coordenada_y_linha_entrada,coordenada_y_linha_saida)):  
            contador_saidas += 1
        #Se necessario, descomentar as lihas abaixo para mostrar os frames utilizados no processamento da imagem
        #cv2.imshow("Frame binarizado", frame_threshold)
        #cv2.waitKey(1);
        #cv2.imshow("Frame com subtracao de background", frame_delta)
        #cv2.waitKey(1);
    print("Contornos encontrados: "+str(qtde_contornos))
    #contabiliza todos os objetos em movimento que entraram e sairam da zona monitorada
    total_objetos_contados = contador_entradas + contador_saidas
    #Escreve na imagem o numero de pessoas que entraram ou sairam da area vigiada
    cv2.putText(Frame, "Entradas: {}".format(str(contador_entradas)), (10, 50),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (250, 0, 1), 2)
    cv2.putText(Frame, "Saidas: {}".format(str(contador_saidas)), (10, 70),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
    cv2.imshow("Original", Frame)
    cv2.waitKey(1);
    #Verifica se deve enviar a contagem para o ThingSpeak
    if (int(time.time()) - timestamp_envio_thingspeak >= 15):
        params = urllib.parse.urlencode({'field1': total_objetos_contados, 'key':write_api_key_thingspeak})
        headers = {"Content-typZZe": "application/x-www-form-urlencoded","Accept": "text/plain"}
        conn = http.client.HTTPConnection("api.thingspeak.com:80")
        try:
            conn.request("POST", "/update", params, headers)
            response = conn.getresponse()
            print (response.status, response.reason)
            data = response.read()
            conn.close()
        except:
            print ("Erro ao enviar ao ThingSpeak")
        
        timestamp_envio_thingspeak = int(time.time())
# cleanup the camera and close any open windows
camera.release()
cv2.destroyAllWindows()

 

Salve-o como contador_objetos_movimento.py na sua pasta home. Execute o script com os comandos abaixo:

 

 cd ~

python3 contador_objetos_movimento.py

 

 

Demonstração da parte de contagem de objetos em movimento - GIF Animado

 Segue abaixo uma demonstração da parte de contagem de objetos em movimento deste projeto, no formato de GIF animado:

 

 


 

 

 

 

Monitoramento no ThingSpeak

A figura 9 mostra o monitoramento dos objetos em movimento, sendo este o total de entradas e saídas contabilizados a cada 15 segundos.

 

Figura 9 - total de objetos em movimento (entrando e saindo da zona monitorada) de 15 em 15 segundos
Figura 9 - total de objetos em movimento (entrando e saindo da zona monitorada) de 15 em 15 segundos

 

 

Conclusão

 Neste artigo, você aprendeu a fazer um projeto de contagem de objetos em movimento utilizando visão computacional na Raspberry Pi 3B. Ainda, este projeto contemplou o monitoramento dos objetos em movimento de forma periódica, via plataforma IoT ThingSpeak.

Este projeto abre portas para projetos muito interessantes em visão computacional, como por exemplo monitoramento de clientes em corredores de uma loja, algo que pode levar a melhorias sutis para maximizar o lucro de uma loja.

 Outro projeto muito interessante seria o monitoramento de carros em vias públicas, estabelecendo o número de carros que circulam na via por dia. Essa informação pode ser valiosa para a engenharia de tráfego estimar de forma muito mais eficiente as manutenções preventivas na via e dimensionar a segurança e aplicação de sinalizações e semáforos no local.

 As possibilidades de projetos para Smart Cities são enormes, sendo somente sua imaginação o limite.