FAVORITAR
FecharPlease login

Detectando e contando objetos através de processamento de imagens com OpenCV no Raspberry Pi Zero

Introdução

Muitas pessoas concordam e eu estou com elas, quando dizem que processamento de imagens é uma das áreas mais legais existentes para se explorar na tecnologia.

Esse artigo tem como objetivo dar uma visão para vocês de como iniciar um processamento de imagem, e para a execução desse projeto as tecnologias escolhidas foram: OpenCV e Python.

A compilação do código será feita no terminal do Raspberry Pi Zero, que já está com o Raspberry Pi OS instalado, sendo um sistema operacional baseado em Debian. Para isso é necessário instalar a biblioteca do OpenCV existente para o Python usando os comandos abaixo e seu ambiente básico vai estar pronto.

OpenCV

OpenCV é uma biblioteca de código aberto com o objetivo de auxiliar na área de visão computacional e Machine Learning. Ela nos ajuda a acelerar o processo de aplicações na área de visão computacional e é aí que o processamento de imagens entra em jogo.

No dia 25 de Junho de 2024, ocorrerá o “Seminário de Sistemas Embarcados e IoT 2024“, no Holiday Inn Anhembi — Parque Anhembi, São Paulo–SP.

Garanta seu ingresso

Atualmente, essa biblioteca conta com mais de 18 milhões de downloads e uma comunidade ativa. Empresas como: Google, Microsoft, Sony e entre outras, utilizam fortemente OpenCV.

Essa biblioteca conta com mais de 2500 algoritmos que nos auxiliam na produção de nossas aplicações, sejam elas em tempo real ou não.

Ela foi criada na linguagem C++, porém, é possível também utilizá-la em linguagens como: Python, Java e Javascript.

Algumas possíveis aplicações que podemos construir utilizando OpenCV:

  • Detecção e reconhecimento de doenças em imagens como tomografias, raio-X, etc…
  • Uso para diversas situações agronômicas, como : Separação/Captação de Alimentos, plantio de sementes, detecção de ervas daninhas, etc. 
  • Detecção de Incêndios.
  • Análise termográficas.
  • Reconstrução e Reconhecimento de Faces.

Esses foram alguns exemplos, mas hoje em dia é possível utilizar o processamento de imagens com outras tecnologias e construir praticamente quaisquer tipos de aplicações, como, por exemplo, no GIF a seguir temos a separação dos tomates por cor seguindo a premissa de processamento de imagens.

Para saber mais sobre a história da mesma e informações técnicas, recomendo que acessem o site: https://opencv.org/.

Observação: A biblioteca OpenCV vai nos facilitar programar o processamento do inicio ao fim, mas lembrem-se: o uso de bibliotecas não tira a importância de entender o que está acontecendo quando chamamos uma função dela, por isso é muito importante que se aprofundem mais no conteúdo citado neste artigo.

Projeto

A explicação desse projeto será um passo a passo e espero que consiga fazer vocês processarem sua primeira (ou não) imagem.

O objetivo do projeto é encontrar, segmentar, contabilizar e marcar os M&M’s (objetos) da nossa imagem, para isso iremos processar a imagem passo a passo para chegarmos no resultado final. A imagem escolhida contém M&M’s e outros objetos como milhos e feijões considerados ruídos para o processamento, pois não queremos procurar e contabilizar os mesmos.

Ao final do artigo, terá alguns outros links que mostrarão códigos de outras imagens com M&M’s em posições e situações diferentes da imagem usada aqui.

1 – Abrindo a imagem que vai ser processada

Esse é o processo mais simples de todo o projeto, consistindo somente na chamada da função cv2.imread, conforme o exemplo a seguir: 

Como visto no exemplo, a função recebe como parâmetro o caminho necessário para encontrar sua imagem nos seus arquivos, exemplo:

Abaixo segue a imagem que será usada para fazer o processamento do artigo:

Lembrando que as imagens podem ocupar muito espaço de armazenamento e quanto maior elas sejam, maior será o uso de poder computacional para processá-las, tendo que prestar atenção a estes pontos quando está trabalhando com o Pi Zero. Para isso o passo a seguir tende a ser indispensável.

2 – Redimensionando a imagem que vai ser processada

Para o redimensionamento, foram criadas duas variáveis onde cada uma receberá os valores respectivamente da altura e largura desejada e após isso, estes mesmos valores são os parâmetros da função cv2.resize que mudará de fato a imagem para o tamanho que queremos.

O redimensionamento foi essencial neste caso, pois a imagem original apresentava dimensões excessivamente grandes, o que prejudicava o processamento em termos de desempenho. Quanto maior a dimensão, mais pesado se torna o processamento, especialmente dependendo das capacidades técnicas do dispositivo utilizado. Além disso, a visualização durante o trabalho pode ser comprometida caso a tela seja menor que o tamanho original da imagem, embora essa decisão seja subjetiva e dependa das preferências pessoais.

Neste caso, utilizei como referência os padrões de tamanhos de imagens, também conhecidos como resolução de imagem. Segui o ideal da proporção de aspecto da imagem, que determina a relação entre altura e largura. Para garantir que o redimensionamento fosse proporcional e que a imagem não ficasse distorcida, recorri à função cv2.imshow, que exibe a imagem em uma nova tela, como exemplificado abaixo:

Alguns exemplos de proporções: 

  • 1:1,   exemplo de resolução de imagem – 1080 X 1080.
  • 3:2,   exemplo de resolução de imagem – 1080 X 720.
  • 4:3,   exemplo de resolução de imagem – 1024 X 768.
  • 16:9, exemplo de resolução de imagem – 1920 X 1080.
  • 9:16, exemplo de resolução de imagem – 360 X 640.

A partir desse momento, toda vez que for citado que é necessário usar a imagem que vai ser processada como parâmetro para algo, tenha em mente que se trata da imagem já redimensionada.

3 – Convertendo a imagem de BGR para HSV e achando as cores

A biblioteca OpenCV segue por padrões o sistema de cor BGR que consiste em três canais de cores que são: B – Blue, G – Green, R – Red, canais estes que são encontrados dentro de cada pixel, e assim as cores das imagens são armazenadas com este sistema, seguindo este padrão, o pixel pode variar dentro de cada canal o valor de 0 a 255.

Porém, para um melhor processamento, é possível passar do sistema BGR (cores primárias) para o HSV, este usa os parâmetros: H – Matriz (tonalidade) que varia de 0 a 179 no OpenCV, S – Saturação (intensidade) que varia de 0 a 255 no OpenCV e V – Valor para identificação da cor que também varia de 0 a 255 no OpenCV. Alguns outros softwares podem adotar outros formatos como, por exemplo: S e V variando entre 0 e 100 e H variando até 360 e não 180.

Caso você obtenha os valores neste outro padrão citado acima, basta fazer uma regra de três com o S e o V para obter os valores para o padrão adotado pelo OpenCV, já para o parâmetro H faça uma divisão por 2.

O HSV irá facilitar a nossa busca dos valores de cores dos pixels pois é muito mais fácil encontrar a cor que queremos variando sua saturação e o seu brilho, ao invés de tentarmos encontrar cada valor dos canais BGR que juntos formam a cor que desejamos. É possível visualizar melhor essa tese ao olharmos a imagem abaixo : 

Para fazermos essa transição de BGR para HSV, chamamos a função cv2.cvtColor como no exemplo a seguir.

Nessa função usamos como primeiro parâmetro a imagem redimensionada que iremos processar (alocada na variável resize no código) e o outro parâmetro é uma definição do OpenCV responsável por mudar o padrão de cores da imagem, no caso ela foi convertida de BGR para HSV.

A imagem quando passada para HSV pode assustar um pouco a princípio, porém é muito melhor trabalhar com esses padrão de cores como citado nos parágrafos acima, dito isso a nossa imagem abaixo passar a ser vista dessa forma pelo OpenCV: 

Agora que temos a nossa imagem em HSV, iremos achar as cores dos objetos que desejamos encontrar na imagem.

Existem várias abordagens para realizar esse processo, mas para torná-lo mais eficiente e fácil, optei pelo uso do software Color Picker. Com essa ferramenta, ao posicionar o cursor sobre os pixels da imagem, são gerados os valores nos sistemas RGB e HSV, seguindo o padrão utilizado pelo OpenCV, conforme explicado nos parágrafos anteriores. É importante observar o software ou sistema que você está utilizando, pois alguns podem gerar valores em padrões diferentes, sendo necessário converter esses valores para o padrão utilizado pelo OpenCV.

Assim teremos os dados para que possamos fazer as nossas máscaras (passo 4). Para cada cor devemos pegar os valores iniciais e finais dos parâmetros HSV, ou seja, onde os tons da cor começam a aparecer e onde eles terminam na sua imagem.

Algumas cores como marrom e vermelho, se assemelham bastante nas escalas de cores, ainda mais quando “brincamos” com saturação e brilho e podem ser mais complicadas de obter os valores. Além disso, o marrom, nessa imagem flutuou entre os dois extremos do valor H no OpenCV, sendo eles respectivamente: 0 – 9 e 140 – 180. Por isso, nestes casos, é recomendado encontrar ambos valores para a criação de 2 máscaras marrons que depois virá a se tornar uma só para de fato acertar onde está o objeto com essa cor.

Além disso, muitas dessas imagens são fotografias, incluindo aquelas que utilizaremos para o processamento, e é comum encontrarmos pontos de luz, reflexos, sombras, etc. Isso pode resultar em desafios, pois teremos pontos que podem tender para o branco ou preto sobre os nossos objetos. Para superar esse obstáculo, o parâmetro S do espaço de cores HSV terá que variar entre 0 e 255 no OpenCV (em alguns casos, até 100), possibilitando a captura do branco ou preto presentes dentro do objeto azul, por exemplo.

Claro que há outras formas de se obter esses valores, como, por exemplo: tentativa e erro. Porém, isso fica a critério de cada um!

Após achar os valores, armazene-os em algum local de fácil acesso, até mesmo como comentário temporário no seu código.

4 –  Criação das Máscaras

Como mencionado no passo anterior, será necessário criar uma máscara para utilizar como parâmetro na segmentação da imagem e encontrar o objeto desejado. A máscara atua como um filtro, permitindo apenas as cores especificadas como parâmetro, enquanto todo o restante é ignorado.

Após acharmos as cores no passo anterior, usaremos a função cv2.inRange, como demonstrado no código abaixo:

É interessante notar nas máscaras que, devido à presença de outros objetos, como milhos e feijões, eles são parcialmente encontrados ou não pelas máscaras correspondentes às cores desses itens. Isso ocorre porque as cores desses objetos se assemelham às dos M&M’s. Mais adiante, abordaremos como contornar essa situação, adicionando operações morfológicas ao processamento e camadas de filtro, como o cálculo das áreas dos objetos, tratando esses elementos como ruídos.

A imagem abaixo mostra uma das máscaras, essa no caso é a máscara amarela, onde podemos perceber que a mesma encontrou outros objetos além dos M&M’s: 

Como dito nos passos anteriores, algumas cores, como o marrom, podem necessitar de mais de uma máscara, sempre se atente a este ponto.

Após a criação das máscaras, caso haja duas ou mais da mesma cor, é necessário combiná-las. Para isso, realizaremos uma operação OR, o que significa que teremos um resultado verdadeiro sempre que pelo menos uma das entradas for verdadeira. Isso resultará em uma fusão (também conhecida como mistura ou junção) entre duas máscaras. Portanto, é possível obter uma única saída com N entradas, desde que as entradas tenham o mesmo tamanho e tipo.

Como exemplo, a imagem abaixo mostra a lógica OR com duas máscaras diferentes, sendo uma a máscara azul e a outra verde dos M&M’s, o resultado é uma junção dos M&M’s que podemos encontrar com cada máscara.

Código referente a lógica OR acima:

No projeto, utilizaremos a operação OR para lidar com os M&M’s marrons, pois essa cor abrange dois extremos nos valores H. Conforme explicado anteriormente, ao aplicar a lógica OR, conseguiremos combinar todos os M&M’s marrons encontrados, independentemente de eles se enquadrarem na variância de cores da primeira máscara ou da segunda:

Seguindo o exemplo dos M&M’s marrons, a imagem a seguir é a primeira máscara marrom feita, onde podemos ver de acordo com a imagem original onde foi achado alguns M&M’s, mas não todos, pois os outros se encontram na outra variância de cor explicado nos parágrafos anteriores.

O mesmo procedimento se aplica caso a segunda máscara (imagem abaixo) seja utilizada no lugar da primeira, possibilitando encontrar os demais M&M’s marrons restantes:

Porém, quando usamos o OR para a junção das duas máscaras, nos retorna a máscara abaixo:

Além disso, na imagem acima, é visível que, além dos M&M’s marrons, alguns M&M’s de outras cores, como os vermelhos, foram encontrados, pois se “assemelham” ao marrom. Essa ocorrência é bastante comum e trataremos dessas situações nas próximas etapas, que abordarão a dilatação e erosão dos objetos.

Para a aplicação dessa lógica usamos a função cv2.bitwise_or, mas não se espante, pois a imagem que a função da lógica irá nos retornar será nada menos que pixels brancos e pretos, ou seja, 1 e 0. 

Na representação numérica, onde 1 (pixels ligados) corresponde aos objetos da imagem marcados de acordo com a determinação de cores que realizamos, e 0 (pixels desligados) indica áreas onde não há nenhum pixel com as cores referenciadas pelas máscaras. Se você executar o comando cv2.imshow para qualquer outra máscara, independentemente do estágio da lógica OR ou não, também visualizará esse tipo de imagem.

5 – (PASSO NÃO OBRIGATÓRIO) Sobreposição de máscaras com lógicas AND ou OR para análise das mesmas 

Podemos verificar se a nossa máscara está bem feita antes de iniciarmos os próximos passos do processamento, para isso podemos fazer uma sobreposição dessa máscara na nossa imagem.

Irei abordar somente duas delas, sendo as lógicas AND e OR.

AND: A lógica AND diz que só é verdadeiro algo desde que todas as entradas sejam verdadeiras, ou seja, a imagem final que será gerada mostrará somente o que tem em comum nas duas entradas que colocamos como parâmetro na função cv2.bitwise_and.

Nesse trecho de código, a função que executa a lógica AND é chamada. Os dois primeiros parâmetros são as imagens tratadas como fontes para o AND. Neste caso, como estamos utilizando apenas uma imagem para extrair as informações, os dois primeiros parâmetros terão o mesmo valor, que é a imagem original que será processada. O terceiro parâmetro, conforme visto na chamada da função, é um parâmetro opcional que representa a máscara. Dessa forma, da imagem original, apenas o que ela e a máscara têm em comum será extraído.

Como exemplo, a imagem abaixo é a saída de uma lógica AND entre a imagem original e a máscara dos M&M’s laranjas, onde também podemos observar a captura de outros objetos que contém em determinada área uma cor situada dentro da variância de cores dos M&M’s laranjas, como, por exemplo: partes dos M&M’s marrons que como já percebemos está na variância vermelho-laranja (considerado então como ruídos também).

OR: A lógica OR já foi explicada no passo anterior (Passo 4) quando foi usada para fazer a junção das duas máscaras marrons criadas.

6 – Dilatação e erosão

Chegamos agora em um dos pontos mais importantes de todo o processo, que é quando vamos tratar pixels indesejados na nossa imagem, separar objetos entre si ou até mesmo “melhorar” nossos objetos, todos esses ruídos nas máscaras citados ao longo do artigo serão retirados e/ou minimizados nessa etapa, faremos algumas operações morfológicas. 

Para começar esse tópico, precisamos falar sobre o parâmetro chamado de Kernel: 

O kernel é como se fosse um objeto que criaremos e a dilatação e/ou erosão acontecerá com base no seu formato (quadrado, circunferência, etc…), podemos chamar ele de elemento estruturante.

Para criar o kernel, estaremos utilizando a biblioteca NumPy. Com ela, conseguimos criar, modificar e acessar vetores e matrizes de forma otimizada.

No código acima é criado uma matriz de formato 3 por 3, ou seja, 3 linhas e 3 colunas formada por valores de número 1. A dimensão 3 por 3, será o formato que será utilizado para erodir e dilatar nossos objetos.

Sabendo disso, podemos então dilatar ou erodir os objetos.

Erosão: Diminui o objeto conforme o seu kernel, usaremos a erosão para eliminar os pixels indesejados que podemos considerar ruídos, aplicado conforme o trecho a seguir: 

No trecho de código acima, a função cv2.erode é utilizada para erodir a imagem conforme necessário. O primeiro parâmetro recebe a máscara de cada cor identificada, o segundo é o kernel, onde declaramos o formato desejado para a erosão na imagem, e, por fim, o terceiro parâmetro representa a quantidade de iterações desejadas para a erosão na imagem. Vale ressaltar que um maior número de iterações resultará em uma erosão mais intensa, ou seja, os objetos se tornarão menores ou mais finos.

Dilatação: A dilatação aumenta o objeto, usaremos para preencher os espaços onde são encontradas algumas falhas de pixels nos objetos achados, ou até mesmo para preencher possíveis objetos que sofreram mais erosão do que o necessário em uma tentativa de retirar os ruídos dos objetos/imagens, aplicado conforme o trecho a seguir: 

O código para executarmos uma dilatação é o mesmo usado para a erosão (explicado logo acima) a única diferença é que ao invés de chamar a função cv2.erode, é chamado a função cv2.dilate, os parâmetros seguem a mesma regra da função da erosão.

Além disso, temos propriedades como Abertura e Fechamento que seguem o mesmo ideal de dilatação e erosão, porém não será abordado neste artigo.

Usando de exemplo a própria máscara marrom após a junção de suas duas partes, é perceptível que existem pixels que queremos anular, pois não são objetos marrons e não queremos contá-los como se fossem. Após aplicarmos a dilatação e erosão, temos o seguinte antes e depois: 

IMAGENS
ANTES DA EROSÃO
DEPOIS DA EROSÃO
DEPOIS DA DILATAÇÃO

7 – Achando os objetos e contornando os mesmos

Agora chegamos de fato, na etapa final do nosso processamento onde usaremos a função cv2.findContours para achar os contornos de todos os M&M’s conforme as respectivas máscaras, a função cv2.countourArea é usada para obter a área dos objetos e fazer uma possível filtragem por tamanho, a função cv2.drawContours irá desenhar em volta de cada M&M achado e a função cv2.putText irá escrever um texto na nossa imagem final, como na imagem abaixo: 

Inicialmente, foram criados dois vetores, o primeiro contendo as strings com o nome das cores a serem inscritas na imagem e o segundo contendo a última parte do processamento de cada cor, no caso, a dilatação dos objetos achados com o filtro de máscara é a última etapa.

O FOR() irá percorrer essas máscaras já processadas e em cada uma será chamado a função cv2.findContours que recebe como parâmetros a máscara processada e os outros dois respectivamente o modo a qual será feito o contorno e o quanto ele deve ser preciso, existem diversos valores para serem usados nesses parâmetros, mas não cabe a esse artigo a explicação.

Usaremos um dos retornos do cv2.findContours, que foi declarado como contornos, ele será usado como argumento da função cv2.counterArea que contabiliza o tamanho da área de cada contorno (objeto), assim caso ainda depois da dilatação e erosão sobre alguma possibilidade de o milho ou feijão serem encontrados, faremos essa exceção do tamanho, pois é evidente que estes objetos têm tamanho diferente dos M&Ms.

Já a função cv2.drawContours irá de fato desenhar os contornos nos nossos objetos e após desenhar todos os objetos de uma imagem chamamos a função cv2.putText que irá escrever a cada execução do FOR() principal, a quantidade de M&Ms por cor na imagem final processada. 

8 – Mostrando a imagem final e salvando-a.

Chegamos no final desse percurso que foi o nosso processamento, agora é só chamar a função cv2.imshow que vai nos mostrar a imagem final abaixo quando executarmos este código, no caso para quem estiver usando o Raspberry Pi Zero é necessário conectá-lo a um equipamento para a saída de imagem, como, por exemplo, um monitor através da sua saída mini-HDMI.

Caso, você não tenha a disponibilidade de um equipamento de saída de imagem, você pode armazenar essa imagem no próprio Raspberry para posteriormente enviar para outro lugar via SSH ou website, assim conseguindo visualizar a imagem final.

E caso queira, salvar essa imagem, execute a função cv2.imwrite, como no exemplo abaixo:

O código completo deste processamento está no link abaixo:

https://github.com/MariaLgA/processamento_objetos/blob/main/processamentoComRuido/processMMs.py

Outras imagens processadas no repositório do GitHub: https://github.com/MariaLgA/processamento_objetos/tree/main

Espero que este artigo tenha contribuído de formas positivas no seu aprendizado! 🙂

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
2 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Lucas Paludo
Lucas Paludo
05/12/2023 00:03

Excelente conteúdo!! Muito interessante

Home » Software » Detectando e contando objetos através de processamento de imagens com OpenCV no Raspberry Pi Zero

EM DESTAQUE

WEBINARS

LEIA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste:


Seminário de
Sistemas Embarcados e IoT 2024
 
Data: 25/06 | Local: Hotel Holiday Inn Anhembi, São Paulo-SP
 
GARANTA SEU INGRESSO

 
close-link