Modelo para identificação dos caracteres da placa

Este post faz parte da série Reconhecimento automático de placas

Este artigo descreve a etapa da identificação dos caracteres da placa de automóveis após a detecção da placa na etapa anterior.  A entrada dessa etapa é o recorte da placa a partir do bouding box obtido na etapa de detecção. Iremos abordar os métodos mais utilizados nesta tarefa, OCR e LPRNet, justificando a escolha da LPRNet. Então, apresentaremos os passos para criação de um dataset sintético com placas de veículos brasileiras e no novo modelo Mercosul para utilizar no treinamento da LPRNet. Por fim, detalharemos sobre o treinamento do modelo LPRNet, mostrando os resultados obtidos. 

OCR vs LPRNet 

Optical Character Recognition (OCR) é uma abordagem bastante popular para reconhecimento de caracteres em várias aplicações. Após obter o recorte da placa detectada, podemos segmentar os caracteres e usar um método de OCR para identificar cada caractere. Muitas implementações já incluem o pré-processamento com segmentação de caracteres no pipeline do OCR, como o EasyOCR.  

Por ser uma rede de propósito geral, e por isso treinada para reconhecer todos os caracteres de um determinado idioma (há vários modelos pré-treinados em Inglês), o OCR é um método pesado computacionalmente. Como alternativa ao OCR, foi proposta a arquitetura de rede neural LPRNet [Zherzdev, 2018], sendo um modelo leve e específico para reconhecimento de caracteres de placas de veículos. A LPRNet recebe a placa inteira como entrada e dá como resultado todos os caracteres da placa. É uma rede simples, sem pré-processamento para segmentação prévia dos caracteres como o pipeline do OCR.  

Dataset Sintético de Placas

A resolução 231/2007 e 509/2016 do CONTRAN especificam as características físicas de placas do modelo antigo e Mercosul, respectivamente. Por simplicidade, iremos nos ater às principais características da placa, como a fonte, tamanho e posicionamento dos textos. Detalhes como as furações e elementos de segurança da placa Mercosul não serão reproduzidos. As Figuras 1 e 2 mostram os resultados obtidos. 

Figura 1: Exemplo de placa sintética do modelo antigo 

Figura 2: Exemplo de placa sintética do modelo Mercosul 

No modelo antigo, a fonte Mandatory é utilizada para os caracteres, nome da cidade e estado. Já o modelo Mercosul utiliza a fonte FE Engschrift para a combinação alfanumérica e a fonte Gill Sans para os demais inscritos. Para ambos os modelos, a placa possui dimensões de 400 mm de largura por 130 mm de altura. 

Podemos descrever ambas as placas de forma genérica: São retângulos preenchidos com uma cor, com cantos arredondados e uma borda de cor diferente. No interior existe uma tarja com um inscrito (nome da cidade ou país), e os caracteres da placa centralizados logo abaixo. Assim, criamos uma classe base com um método genérico de geração de placa que recebe os caracteres da placa e o texto superior (nome da cidade ou do país, no caso do modelo Mercosul).  

A fim de gerar um código mais conciso, utilizaremos a orientação a objetos do Python para criar uma classe base, Plate, que possuirá um método genérico, gen(text, top_text), A partir dessa classe base, criamos duas classes que herdam dela, Antigo e Mercosul, que especializam o método para os modelos antigo do Brasil e Mercosul, respectivamente. 

Além disso, também definimos o método augstr, que gera uma string com base em um padrão de dígitos e letras passado como argumento. Este método também pode receber um vetor de pesos que define a probabilidade de cada caractere do vocabulário definido (letras do alfabeto a-z e dígitos de 0-9) serem sorteados na geração da string. Esses pesos foram definidos com o objetivo de balancear o dataset, buscando gerar um número de exemplos similar para cada um dos caracteres. 

Também desenvolvemos uma série de transformações para trazer variabilidade no dataset com o objetivo de trazer maior robustez ao modelo em treinamento. Por exemplo, a transformação noise adiciona ruído branco a imagem: 

def noise(img): 
    n = (255*np.random.random_sample(img.shape) - 127) * 0.01 
    return np.clip(img + n.astype(img.dtype), 0, 255) 

E a transformação warp sorteia quatro pontos, constrói uma matriz de transformação com cv2.getPerspectiveTransform e então invoca o método cv2.warpPerspective para gerar uma distorção de perspectiva aleatória: 

def warp(img): 
    img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA, img) 
  
    xmax = img.shape[1] 
    ymax = img.shape[0] 
    src = np.array([ 
                     [0, 0],  
                     [0, ymax], 
                     [xmax, ymax],  
                     [xmax, 0] 
                   ], dtype='float32') 
    dst = np.array( 
               [ 
                 [xmax*random.random()/4, ymax*random.random()/4], 
                 [xmax*random.random()/4, ymax*(1-random.random()/4)], 
                 [xmax*(1-random.random()/4), ymax*(1-random.random()/4)], 
                 [xmax*(1-random.random()/4), ymax*random.random()/4] 
               ],  
               dtype='float32') 
  
    dst[:,0] -= min(dst[:,0]) 
    dst[:,1] -= min(dst[:,1]) 
    xmax = math.ceil(max(dst[:,0])) 
    ymax = math.ceil(max(dst[:,1])) 
  
    m = cv2.getPerspectiveTransform(src, dst) 
    img = cv2.warpPerspective(img, m, (xmax, ymax),  
                              np.empty([ymax, xmax, 4]), cv2.INTER_LINEAR) 
  
    bg = np.zeros((*img.shape[:2], 3)) 
    color = np.random.randint(0, high=256, size=3) 
    bg[:] = color 
  
    alpha = img[..., 3] / 255 
    img = img[:, :, :3] * alpha[..., np.newaxis] +  
                          bg * (1 - alpha[..., np.newaxis]) 
    return np.dstack((img, alpha)).astype('uint8') 

Também foram definidas transformações para alterar brilho, saturação e desfoque de forma aleatória. 

Finalmente, o método gen_dataset foi desenvolvido para gerar as placas sintéticas com os caracteres sorteados pela função augstr, aplicar um número aleatório de transformações sobre a placa gerada, e criar o arquivo com a etiqueta da imagem para ser utilizado no treinamento. 

def gen_dataset(batch_num, batch_size, plate_type=plate.Antigo): 
   vocab = {} 
   for c in string.ascii_uppercase + string.digits: 
        vocab[c] = 1 
  
   for batch in range(1, batch_num + 1): 
       weights = {'A': [ 1 / vocab[c] for c in vocab  
if c in string.ascii_uppercase ], 
                   'D': [ 1 / vocab[c] for c in vocab  
if c in string.digits ]} 
  
       for i in range(batch_size): 
           p = plate_type(width = max(100, 1920*random.random())) 
           if plate_type == plate.Mercosul: 
                label = augstr('aaadadd', weights=weights) 
                top_text = 'BRASIL' 
           else: 
                label = augstr('aaa-dddd', weights=weights) 
                top_text = 'SP-CAMPINAS' 
  
           img = np.array(p.gen(label, top_text=top_text)) 
           img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA) 
  
           label = label.replace('-', '') 
           for c in label: 
                if c in vocab: 
                    vocab[c] += 1 
  
           num_transforms = len(transform) * random.random() 
           for t in set(random.choices(transform, k=int(num_transforms))): 
                 img = t(img) 
  
           aspect_ratio = img.shape[1]/img.shape[0] 
           if((aspect_ratio > 2+epsilon) or (aspect_ratio < 2-epsilon)): 
                    new_width = 2 * img.shape[0] 
                resized_img = cv2.resize(img, (new_width, img.shape[0]),     
    interpolation= cv2.INTER_LINEAR) 
                img = np.copy(resized_img) 
   
            cv2.imwrite(‘output/' + label + '.jpg', img) 
            with open('output/' + label + '.txt', 'w') as f: 
                print(label, file=f) 

Os argumentos batch_num e batch_size indicam, respectivamente, o número e tamanho dos batches de imagens a serem gerados. Para promover o balanceamento entre as classes, a lista de pesos utilizada pelo método augstr é atualizada entre cada batch com o inverso da frequência dos caracteres. A divisão em batches evita esse cálculo a cada imagem, e também facilitará a paralelização em versão futuras do código. O dicionário vocab é utilizado para contar o número de ocorrências de cada caractere. Para evitar uma divisão por zero, todas as chaves do dicionário são inicializadas com o valor um. 

Para cada imagem do batch, uma placa com largura entre 100 e 1920 pixels é construída com os caracteres sorteados por augstr. Em seguida, um número aleatório de transformações é aplicado na imagem obtida. A imagem final é ajustada para as dimensões de entrada da rede LPRNet (96×48) e salva no diretório output/, utilizando os caracteres sorteados como nome do arquivo. Para facilitar a utilização com alguns scripts de treinamento, um arquivo de texto com o mesmo nome da imagem também é gerado, contendo apenas os caracteres da placa. 

A Figura 3 apresenta alguns exemplos de placas geradas para o dataset sintético de placas do modelo antigo e Mercosul.

Exemplos de placas  
Figura 3: Exemplos de placas   

Para realizar o treinamento e validação do modelo de reconhecimento de caracteres LPRNet, geramos 600 batches de 100 imagens do modelo antigo e 600 de 100 imagens do modelo Mercosul, dividindo essas imagens igualmente em três conjuntos: treinamento, validação e teste. Esse dataset gerado está disponível na plataforma Kaggle

Treinamento da LPRNet 

Nós utilizamos o modelo LPRNet disponilizado pela NVIDIA, pois o treinamento pode ser realizado usando o Toolkit NVIDIA TAO, e este toolkit também pode ser usado para exportar o modelo treinado em TensorRT, além de uma ampla documentação oficial. A NVIDIA disponibiliza alguns containers Docker com o TAO já instalado e alguns modelos configurados. Para o caso da LPRNet, é disponibilizado um container com o TAO para visão computacional, o TAO Toolkit for CV. Utilizamos a versão v3.21.08-py3 do container, a qual contém a versão CUDA 11.4 e TensorRT 8.0.1. Dessa forma, para utilizar o container Docker (para instalação e configuração do Docker consultar a seguinte documentação) use o comando para realizar o pull da imagem:

$ docker pull nvcr.io/nvidia/tao/tao-toolkit-tf:v3.21.08-py3

Então, o container pode ser criado e executado a partir dessa imagem:

$ docker run --rm -ti --name lprnet_trt \ 
   -it nvcr.io/nvidia/tensorrt:21.07-py3 /bin/bash 

Dentro do container, é necessário organizar os arquivos necessários para treinamento do modelo. Primeiro, criamos uma pasta chamada lprnet dentro do diretório /workspace, e então organizamos o dataset com estrutura no formato apresentado na Figura 4, em /workspace/lprnet

Figura 4: Estrutura do diretório do dataset, com as imagens e labels separadas em diferentes pastas. 

Tanto o conjunto de treinamento como de teste deve seguir essa estrutura. Criamos ambos no diretório /workspace/lprnet/dataset_lprnet_antigo_mercosul/

Após organizar os conjuntos de treinamento e validação, criamos o diretório /workspace/lprnet/lprnet_trainable para armazenas os pesos pré-treinados e o arquivo com a lista de caracteres do modelo, ambos disponibilizados neste link. Ao realizar o download dos arquivos, extrai-os no diretório criado. Usamos o modelo pré-treinado com placas de veículos dos Estados Unidos, us_lprnet_baseline18_trainable.tlt. Alteramos o arquivo com a lista de caracteres corresponde, us_lp_characters.txt, para incluir o caractere O e usamos esse arquivo para treinar o modelo com placas antigas e do Mercosul. 

Depois dessas etapas, é necessário criar um arquivo com as especificações do treinamento, como detalhado na seção Creating an Experiment Spec File. Criamos o arquivo /workspace/lprnet/specs/lprnet_spec.txt, copiando o exemplo apresentado neste link e alterando os seguintes parâmetros: 

max_label_length: no caso das placas de veículos antigas do Brasil e as do Mercosul, esse valor deve ser 7

nlayers: como usamos o modelo pré-treinado us_lprnet_baseline18_trainable.tlt, esse parâmetro deve ser 18

batch_size_per_gpu: aumentamos esse valor para 64

num_epochs: reduzimos esse valor para 50, pois estamos fazendo fine-tuning; 

image_directory_path de data_sources: caminho para a pasta contendo as imagens de treinamento; 

label_directory_path de data_sources: caminho para a pasta contendo os labels de treinamento; 

image_directory_path de validation_data_sources: caminho para a pasta contendo as imagens de validação; 

label_directory_path de validation_data_sources:  caminho para a pasta contendo os labels de validação; 

characters_list_file:  caminho para o arquivo com a lista de caracteres. 

A Figura 5 apresenta a estrutura final do arquivo lprnet_spec.txt, com os novos valores dos parâmetros descritos acima. 

Figura 5: Estrutura do arquivo lprnet_spec.txt após alterar o valor dos parâmetros descritos. 

Ao concluir esses passos, deve-se criar uma pasta para salvar os pesos gerado durante o treinamento, criamos a pasta /workspace/lprnet/experiment_dir_unpruned. Então, o treinamento do modelo LPRNet para placas antigas brasileiras e do Mercosul pode ser realizado com o seguinte comando:

$ lprnet train --gpus <NGPUS>       
         -e /workspace/lprnet/specs/tutorial_spec.txt / 
               -r /workspace/lprnet/experiment_dir_unpruned/ -k nvidia_tlt / 
     -m /workspace/lprnet/lprnet_trainable/us_lprnet_baseline18_trainable.tlt 

Utilizamos os seguintes parâmetros: 

-e: passa o arquivo com as especificações de treinamento  

-r: indica o diretório para salvar os modelos gerados durante o treinamento 

-m: passa o modelo pré-treinado a ser utilizado no treinamento 

–gpus: indica o número de GPUs <NGPUS> a ser utilizado no treinamento. Se desejar utilizar apenas uma, identificar através do parâmetro –gpu_index=<IDX_GPU>, substituindo <IDX_GPU> pelo índice da GPU, o qual pode ser obtido com o comando nvidia-smi. Mais detalhes sobre os parâmetros do comando train podem ser encontrados na seguinte documentação.  

Os modelos gerados ao longo do treinamento, com extensão .tlt, são salvos no diretório passado com o parâmetro -r, neste caso /workspace/lprnet/experiment_dir_unpruned/. O processo de treinamento também gera um arquivo de log training_log.csv com os valores de loss do conjunto de treinamento calculada a cada época e as acurácias do conjunto de validação obtidas a cada cinco épocas. As Figuras 6 e 7 mostram os gráficos gerados a partir do histórico da loss de treinamento e do histórico da acurácia de validação, respectivamente. 

Figura 6: Histórico da loss do conjunto de treinamento nas 50 épocas. 

Figura 7: Histórico da acurácia do conjunto de validação calculada a cada 5 épocas. 

Observando os gráficos apresentados, notamos que a loss de treinamento estabiliza a partir de 15 épocas. Já a acurácia de validação fica estável a partir de 30 épocas. Dessa forma, escolhemos o modelo salvo na 30ª época. Avaliamos esse modelo escolhido com o conjunto de teste. Para isso, primeiro é necessário alterar os parâmetros image_directory_path e label_directory_path de validation_data_sources do arquivo lprnet_spec.txt, indicando o caminho dos labels e imagens do conjunto de teste, respectivamente: 

Após configurar os caminhos para o conjunto de teste, basta executar o seguinte comando: 

$ lprnet evaluate --gpu_index=<IDX_GPU>  /
     -e /workspace/lprnet/specs/tutorial_spec.txt / 
           -k nvidia_tlt / 
   -m /workspace/lprnet/experiment_dir_unpruned/lprnet_epoch-30.tlt 

Os parâmetros utilizados foram: 

-e: passa o arquivo com as especificações do experimento  

-m: indica o modelo que desejamos avaliar 

–gpu_index: identifica o índice da CPU <IDX_GPU> que será utilizada para avaliar o modelo, o qual pode ser obtido com o comando: $ nvidia-smi

Este comando executa a inferência para as 12 mil imagens de teste, calculando a acurácia de acordo com os labels de teste. Obtivemos uma acurácia de aproximadamente 99,7%, o que é um resultado bem próximo ao obtido com o conjunto de validação. Isso indica que o modelo conseguiu aprender os padrões das placas e generalizar para um novo conjunto. 

Para ser otimizado para a Jetson Nano, vamos exportar esse modelo em TensorRT. Para isso, precisamos primeiro exportar o modelo na extensão .tlt para a extensão .etlt para deployment. Esse processo é realizado usando o seguinte comando: 

$ lprnet export --gpu_index=<IDX_GPU> \  
  -m /workspace/lprnet/experiment_dir_unpruned/lprnet_epoch-30.tlt \ 
        -k nvidia_tlt \  
        -e /workspace/lprnet/specs/lprnet_spec.txt \  
      -o /workspace/lprnet/experiment_dir_unpruned/export/lprnet_model.etlt    

Utilizamos os seguintes parâmetros: 

-m: passa o modelo na extensão .tlt que queremos exportar 

-e: passa o arquivo com as especificações do experimento  

-o: indica o diretório e o nome do modelo que será exportado para extensão .etlt 

–gpu_index: identifica o índice da CPU <IDX_GPU> que será utilizada para exportar o modelo, o qual pode ser obtido com o comando nvidia-smi

Mais detalhes sobre os parâmetros do comando export podem ser encontrados na seguinte documentação. Ao final da execução do comando acima, o modelo lprnet_model.etlt é gerado no diretório /workspace/lprnet/experiment_dir_unpruned/export/. Disponibilizamos o modelo treinado e exportado no arquivo modelos.zip, dentro da pasta lprnet

Próximos passos

No artigo anterior da série apresentamos como treinar um modelo de detecção de placas de automóveis usando a YOLOv4-tiny. Neste artigo mostramos os passos para gerar um dataset de placas sintéticas e treinar um modelo de reconhecimento de placas de veículos usando a LPRNet. Este é o segundo passo para o reconhecimento automático dessas placas, em que a partir da placa detectada no passo anterior, são reconhecidos os caracteres desta. No próximo artigo abordaremos a implementação destes passos usando a Jetson Nano. 

[Wikipedia Typeface, 2021] Wikipedia. Typeface: Font metrics. URL: https://en.wikipedia.org/wiki/Typeface#Font_metrics. Acessado em: 28/01/2022. 

[Zherzdev, 2018] Sergey Zherzdev e Alexey Gruzdev. Lprnet: License plate recognition via deep neural networks. arXiv preprint arXiv:1806.10447 (2018). 

Autores

  • Geise Santos
  • Matheus Kowalczuk Ferst
  • Elton de Alencar
  • Lucas Coutinho
  • Murilo Pechoto

Reconhecimento automático de placas

Implementação na Jetson NanoReconhecimento automático de placas de identificação de veículos em uma Jetson Nano usando YOLOv4-tiny e LPRNet >>
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
4 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Renan
Renan
23/01/2024 18:52

Poderia gerar dados sintéticos para placa de moto?

Ricardo
Ricardo
25/02/2023 18:01

Parabéns pelo belo artigo e tutorial, isso sim gera conhecimento e não só informação. Ansioso pela próxima etapa.
Você acham que um raspa Pi pode rodar esse programa para identificação de placas para um controle de entrada e saída de condomínio ou monitorar fluxo de veículos em ruas para fins de estudo de segurança pública?

Wígny Almeida
Wígny Almeida
30/12/2022 17:18

Olá, o link para o dataset sintético de placas (https://www.kaggle.com/dataset/0bca3d415403a5174de8f1b62af3512b1e4f848f8bb76b371963f5504bb7951f) não está funcionando, seria possível a disponibilização do dataset? (Talvez o Hugging Face seja uma boa opção para a disponibilização do dataset.)

Home » Software » Modelo para identificação dos caracteres da placa

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: