ÍNDICE DE CONTEÚDO
- Modelo para identificação dos caracteres da placa
- Implementação na Jetson Nano
- Reconhecimento automático de placas de identificação de veículos em uma Jetson Nano usando YOLOv4-tiny e LPRNet
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.
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:
1 2 3 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
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.
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:
1 |
$ 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:
1 2 |
$ 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.
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.
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:
1 2 3 4 |
$ 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.
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:
1 2 3 4 |
$ 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:
1 2 3 4 5 |
$ 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