Blog

Detecção e reconhecimento de placas com OpenCV, Python e Google Vision API

por Vinícius Bôscoa||6 min de leitura

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:

  1. Contém exclusivamente 1 placa e;
  2. 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:

  1. Converter a imagem para grayscale (escala de cinza);
  2. Aplicar a transformação morfológica Black Hat - com o objetivo de revelar caracteres escuros contra fundos claros;
  3. Operação de fechamento (close) para preencher gaps e áreas pequenas - a fim de identificar estruturas maiores;
  4. Calcular o Gradiente de magnitude (eixo x), considerando a imagem da transformação Black Hat - Colocaremos o resultado na escala [0, 255]
  5. 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:

  1. O usuário envia uma imagem e clica em um dos botões "Processar" ou "Detectar Texto"
  2. Se clicar na opção "Processar" - a aplicação exibe a imagem da placa.
  3. 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ção
  • core.py - classe responsável pelas transformações e extração da placa
  • detector.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:

Car vector created by vectorpouch - www.freepik.com