Já pensou na possibilidade de localizar e extrair placas de carros utilizando apenas processamento de imagens? E extrair o texto dessa imagem? Neste artigo iremos fazer isso.
Bônus: com a placa extraída iremos enviá-la para a API do Google (Google Vision AI) que irá reconhecer os dígitos e retorna-los como texto.
Requisitos
Para nossa aplicação estamos supondo que a imagem:
- Contém exclusivamente 1 placa e;
- O padrão da placa será Mercosul (letras escuras em um fundo claro).
O algoritmo
Para a detecção e extração da placa utilizaremos o seguinte fluxo de transformações que serão detalhadas no código:
- Converter a imagem para grayscale (escala de cinza);
- Aplicar a transformação morfológica Black Hat - com o objetivo de revelar caracteres escuros contra fundos claros;
- Operação de fechamento (close) para preencher gaps e áreas pequenas - a fim de identificar estruturas maiores;
- Calcular o Gradiente de magnitude (eixo x), considerando a imagem da transformação Black Hat - Colocaremos o resultado na escala [0, 255]
- Suavizar a imagem, aplicar uma transformação de fechamento e outro threshold binário utilizando o método Otsu.
Fluxo da aplicação
A aplicação terá as seguintes funcionalidades:
- O usuário envia uma imagem e clica em um dos botões "Processar" ou "Detectar Texto"
- Se clicar na opção "Processar" - a aplicação exibe a imagem da placa.
- Se clicar na opção "Detectar Texto" - a aplicação exibe a imagem da placa e o texto reconhecido.
Mão no Código
Para simplificar, iremos utilizar o streamlit para construir nossa aplicação, dessa forma, nos concentramos apenas no que interessa - Processamento de Imagens.
Iremos dividir a aplicação em duas etapas: extração e detecção.
Estruturamos nossa aplicação em 3 arquivos:
main.py
- que será o ponto inicial da aplicaçãocore.py
- classe responsável pelas transformações e extração da placadetector.py
- que abrigará a função responsável por enviar a imagem ao Google Vision API e retornar o texto detectado
First things first
Como de costume, primeiro instalamos os pacotes necessários.
pip install opencv-python numpy streamlit
Com os pacotes instalados, começamos a construir o fluxo de execução da aplicação pelo main
.
# importar pacotes
import streamlit as st
from core import Extractor
from PIL import Image
def main():
# título e descrição na página principal
st.title('Extrator de placas veiculares')
st.markdown("""
### Instruções
Faça o upload de uma imagem e clique em **processar**.
Nós tentaremos extrair **somente** a placa. (utilizaremos como base o padrão mercosul)
""")
st.info('Para um melhor resultado, recomendamos enviar imagens em alta resolução')
# componente do stremlit responsável pelo envio da imagem
image_file = st.file_uploader("Envie a imagem", type=['png', 'jpg', 'jpeg'])
if image_file:
# carregamos a imagem
image = Image.open(image_file)
# inserimos um botão do streamlit
# se o botão for clicado, ele retornará True
if st.button("Processar"):
# instanciamos a classe extractor
extractor = Extractor(image)
# executamos a classe
extracted = extractor()
# se alguma placa foi extraída, exibe a imagem
if extracted is not None:
st.text("Placa extraída da imagem:")
st.image(extracted)
# do contrário exibe mensagem de erro
else:
st.error('Não conseguimos extrair a placa')
else:
st.text('Faça o upload de uma imagem')
if __name__ == '__main__':
main()
O arquivo main
é bastante simples, nele estamos apenas configurando nossa página e instanciando nossa classe principal extractor
.
Dividir e conquistar
Com as responsabilidades dividas, iremos agora ver nossa classe extractor
:
# importar pacotes
import numpy as np
import cv2
import imutils
# definimos um kernel principal (utilizaremos esse kernel em mais de uma função)
main_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (40, 13))
# definição da classe
class Extractor:
# definição do valor default dos parâmetros
erode_iter = 2
dilate_iter = 1
# inicialização da classe
def __init__(self, image):
self.original_image = image
self.converted_image = np.array(image)
# execução da classe
def __call__(self, erode_iter=2, dilate_iter=1):
# definição dos parâmetros de erosão e dilatação (que serão utilizados no fine_tunning)
self.erode_iter = erode_iter
self.dilate_iter = dilate_iter
# converter a imagem para grayscale
image = self.to_grayscale()
# extrair a área da placa
area = self.get_plate_area()
# localizar contornos a partir da área
contours = cv2.findContours(area.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contours = imutils.grab_contours(contours)
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:5]
# verifica, para cada contorno encontrado, a proporção, se estiver entre 2.5 e 4, supõe-se que seja a placa
for c in contours:
(x, y, w, h) = cv2.boundingRect(c)
ratio = w / h
# dimensões da placa: 40x13cm
if 2.5 <= ratio <= 4:
identified_plate_area = image[y: y + h, x: x + w]
cropped_plate = cv2.threshold(identified_plate_area, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[-1]
return cropped_plate
# função de transformação em grayscale
def to_grayscale(self):
gray_image = cv2.cvtColor(self.converted_image, cv2.COLOR_RGB2GRAY)
gray_image = cv2.bilateralFilter(gray_image, 13, 15, 15)
return gray_image
# sequencia de transformações que resultarão na área da placa
def get_plate_area(self):
black_hat_image = self._black_hat_morph()
# light_image = self.close(gray_image)
gradient_image = self._magnitude_gradient(black_hat_image)
smoothed_image = self._smooth(gradient_image)
area_image = self._fine_tunning(smoothed_image)
return area_image
# transformação black hat
def _black_hat_morph(self):
gray = self.to_grayscale()
black_hat = cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, main_kernel)
return black_hat
# operação de fechamento
def _close(self, image):
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
light = cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel)
light = cv2.threshold(light, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[-1]
return light
# calculo do gradiente de magnitude
def _magnitude_gradient(self, image):
gradient_x = cv2.Sobel(image, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)
gradient_x = np.absolute(gradient_x)
# extrair valores mínimo e máximo
minimo, maximo = np.min(gradient_x), np.max(gradient_x)
# normalizar = (valor-min) / (max-min)
gradient_x = 255 * (gradient_x - minimo) / (maximo - minimo)
# converter para UINT8
gradient_x = gradient_x.astype("uint8")
return gradient_x
# suavização
def _smooth(self, image):
gradient_x = cv2.GaussianBlur(image, (5, 5), 0)
gradient_x = cv2.morphologyEx(gradient_x, cv2.MORPH_CLOSE, main_kernel)
thresh = cv2.threshold(gradient_x, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[-1]
return thresh
# operação de fechamento final
def _fine_tunning(self, image):
thresh = cv2.erode(image, None, iterations=self.erode_iter)
thresh = cv2.dilate(thresh, None, iterations=self.dilate_iter)
return thresh
Não iremos detalhar cada uma das funções disponíveis no OpenCV, nem a sequência de técnicas, uma vez que podemos obter o mesmo ou até melhores resultados apenas variando as combinações das técnicas. Portanto iremos nos concentrar nas duas funções principais: init
e call
.
A função init
irá apenas receber a imagem que foi enviada pelo usuário e irá gerar uma versão em matriz da mesma salvando em self.converted_imagem
, essa imagem será utilizada como base para as transformações.
O método call
é responsável por executar as transformações na imagem original, localizar a possível posição da placa e recortá-la da imagem, deixando apenas a área que nos interessa. Esse método aceita 2 parâmetros (quantidade de iterações da erosão e dilatação, respectivamente). Esses parâmetros tentam melhorar a precisão da detecção da área de interesse (nesse caso, da placa).
Mostre-me o texto!
Com a placa extraída, podemos utilizar a API do Google Vision AI que é capaz de reconhecer texto em imagens, objetos em imagens, conteúdo explícito, rotulagem , detecção de rostos, entre outros recursos. Utilizaremos o recurso de OCR para detectar o texto da nossa imagem.
Começamos instalando a biblioteca google-cloud-vision via pip:
pip install --upgrade google-cloud-vision
Para o Google Vision funcionar corretamente, também é preciso configurar o serviço na Google Cloud Platform. O passo a passo pode ser visto neste link.
Iremos agora trabalhar no nosso detector
:
# importar pacotes
import cv2
def detect_text(image):
"""Detects text in the file."""
# importar client do vision AI
from google.cloud import vision
# instanciar client
client = vision.ImageAnnotatorClient()
# converter imagem para jpg
success, encoded_image = cv2.imencode('.jpg', image)
# transformar a imagem em bytes
content = encoded_image.tobytes()
# gera imagem que será utilizada pelo client
image = vision.Image(content=content)
# executar detecção de texto
response = client.text_detection(image=image)
# capturar textos encontrados
texts = response.text_annotations
# em caso de erro na API
if response.error.message:
raise Exception(
'{}\nFor more info on error messages, check: '
'https://cloud.google.com/apis/design/errors'.format(
response.error.message))
# retorna a descrição do primeiro texto encontrado
return texts[0].description
Utilizamos a maior parte do código de exemplo do Vision AI, mas o que está acontecendo?
Em nossa função detect_text
, que espera receber uma imagem como parâmetro, importamos a biblioteca do Google Vision, em seguida instanciamos o ImageAnnotatorClient
essa classe será responsável por fazer a comunicação com a API.
Antes de enviarmos nossa imagem, precisamos convertê-la, primeiro convertemos em jpg e em seguida extraímos seu conteúdo em bytes encoded_image.tobytes()
para, então, criarmos a imagem que será utilizada pelo client da API vision.Image(content=content)
.
client.text_detection(image=image)
: esse comando irá enviar a imagem à API e receber, em caso de sucesso, o texto reconhecido.
Juntando as partes
Por fim combinamos nossa função detect_text
no main
, no final do if image_file
from detector import detect_text
# previous code
if st.button('Detectar texto'):
extractor = Extractor(image)
extracted = extractor()
try:
if extracted is None:
raise Exception("Não conseguimos extrair nenhuma placa")
st.text("Placa extraída da imagem:")
st.image(extracted)
text = detect_text(extracted)
st.success(f'Texto extraído: {text}')
except Exception as exc:
st.error(str(exc))
# rest of code
Projeto
O código fonte do projeto pode ser encontrado aqui.
Demo
Uma demo da aplicação pode ser vista nesse link.
A funcionalidade de detecção de texto não está disponível na demo, mas deixamos um exemplo do funcionamento abaixo:

Referências
https://docs.opencv.org/4.5.2/d9/d61/tutorial_py_morphological_ops.html
https://www.geeksforgeeks.org/top-hat-and-black-hat-transform-using-python-opencv/
https://learnopencv.com/otsu-thresholding-with-opencv/
Imagem do Post: