Treinando um modelo de OCR com dados sintéticos

Felipe Crispim da Rocha Salvagnini
Data Hackers
Published in
10 min readJun 26, 2020

Utilizando dados sintéticos para treinar um modelo de reconhecimento dos caracteres de placas veiculares

Motivação

No desenvolvimento de aplicações de visão computacional uma etapa que comumente consome tempo e recursos financeiros é a etapa de construção de datasets, que normalmente requer:

  • A aquisição de imagens;
  • A anotação de cada classe de imagem para problemas de classificação;
  • A anotação da posição dos objetos nas imagens (Seja em problemas de detecção de objetos ou de segmentação).

Existem algumas ferramentas que podem acelerar essas etapas, algumas open source (Ex.: LOST) e algumas pagas, onde é possível terceirizar a anotação do dataset (Ex.: Amazon Mechanical Turk). Estas ferramentas estão fora do escopo do artigo.

Uma abordagem que merece atenção especial, quando possível, é a geração de dados sintéticos, que representem com a maior fidelidade possível o problema real que se busca resolver. Assim, é possível também acelerar o processo de anotação de dados, uma vez que na maioria das vezes durante o processo de geração das imagens já é possível separá-las em classes, ou indicar a posição dos objetos nas mesmas.

Este artigo busca demonstrar como podemos treinar um modelo para o problema de leitura dos caracteres da placa de um veículo, sem a necessidade de termos um dataset com imagens reais.

Repositório e replicação do código

Os scripts python e o notebook de treinamento do modelo podem ser encontrados neste repositório.

Na pasta dockerfile é possível encontrar os arquivos para replicar os resultados aqui demonstrados utilizando containers. Para criar o container com todas as dependências necessárias execute o script execute_compose.sh.

Observação: O container foi testado no ubuntu 20.04, pode ser que em outras distros ocorram algum problema , devido a necessidade do container utilizar a interface gráfica do host (X11).

Entendendo o problema

Como o problema que resolveremos é o reconhecimento de cada caractere individualmente, a abordagem será gerar imagens com números e letras aleatórios, atendendo os seguintes requisitos:

  • Letras maiúsculas;
  • Mesma fonte utilizada nas placas veiculares;

Atualmente no Brasil temos dois padrões de placa em circulação, com fontes diferentes:

Levaremos em consideração que a maior parte dos veículos em circulação utilizam o padrão antigo portanto, utilizaremos apenas a fonte Mandatory. O download da fonte pode ser efetuado no site dafont.com.

Gerando imagens sintéticas

Para gerar as imagens sintéticas, empregaremos a ferramenta TextRecognitionDataGenerator (trdg), descrita como um gerador de dados sintéticos para reconhecimento de texto.

Para instalar o trdg iremos utilizar o pip.

pip install trdg

Para gerar as imagens executamos a ferramenta através da linha de comando, conforme indicado abaixo:

trdg -c 20000 --case upper -rs --include_letters --include_numbers -w 1 -f 101 --font mandatory/Mandatory.ttf --output_dir out/

Onde, a geração de imagens é configurada da seguinte maneira:

  • -c: Número de imagens a ser criado;
  • — case upper: Gera caracteres maiúsculos apenas;
  • -rs: Configura as sequências randômicas para serem utilizadas na geração. Iremos configurar para incluir apenas números ( — include_numbers) e letras ( — include_letters);
  • -f: Define a altura da imagem produzida (Em pixels);
  • — font: Arquivo especificando a fonte a ser utilizada.

Exemplos de imagens geradas:

AA662_5027.jpg
FD7BY3QA_6006.jpg

Observe que o nome da imagem é organizado da seguinte maneira: parte1_parte2.jpg. As letras da parte1 são nossas labels e a parte2 indica o índice da imagem gerada.

Criando um dataset das imagens no formato hdf5

Os códigos desta seção podem ser encontrados no script export_hdf5_file.py.

O formato hdf5

Um formato de dados hierárquicos, o hdf (do inglês Hierarchical Data Format), foi criado em 1987 como uma opção para trabalhar com grandes volumes de dados científicos, de forma portátil e compacta.

Cada arquivo .hdf5 pode conter datasets e grupos, onde datasets são arrays de n-dimensões e grupos podem conter múltiplos datasets ou ainda outros grupos.

Utilizaremos a biblioteca h5py para trabalhar com o hdf5 utilizando Python. Para instala-la execute o comando abaixo:

pip install h5py

Geração do dataset

Para a geração do dataset, iremos utilizar a classeH5pyAcess:

Ao ser instanciado um objeto de acesso a um arquivo hdf5 é necessário indicarmos 3 parâmetros:

  • path_to_save: Caminho para o arquivo hdf5 a ser escrito;
  • buffer_size: Tamanho do buffer de dados para escrevermos no arquivo hdf5. Isto é, quando a quantidade de dados no buffer for maior que o valor de buffer_size iremos escreve-los no arquivo hdf5 e limpar o buffer. Desta forma é possível evitar consumo excessivo de memória;
  • data_shape: O formato das imagens, iremos utilizar a seguinte tupla (119705, 80, 55, 3). Onde 119705 é a quantidade de imagens de caracteres, e também de labels, 80 e 55, são a altura e a largura dos caracteres respectivamente, e 3 é o número de canais das imagens.

Quanto aos métodos disponíveis para o objeto criado temos:

  • add_data: Responsável por adicionar dados aos buffer, caso a quantidade de dados no buffer for maior que buffer_size, chama o método writes_to_disk;
  • writes_to_disk: Escreve os dados nos datasets do arquivo hdf5 (Caracteres e labels) e limpa o conteúdo do buffer;
  • end_connection: Finaliza o acesso ao arquivo hdf5. Caso o buffer não esteja vazio, escreve seu conteúdo no arquivo hdf5 antes.

A escrita da classe foi inspirada em um post do blog sigmoidal, você pode acessar o post aqui.

Por fim, vale comentar que uma das maiores motivações para o uso do hdf5, foi a performance do mesmo quando trabalhando com um grande volume de imagens. Neste post do Real Python é possível visualizar uma comparação de desempenho com outras abordagens para trabalhar com leitura e escrita de imagens.

Segmentação dos caracteres

Para a segmentação dos caracteres foram adotadas as seguintes preocupações:

  • A imagem de cada caractere deve possuir um tamanho de 80 de altura e 55 de largura;
  • Utilizar o nome da própria imagem, para gerar as labels de cada caractere.

O problema foi abordado da seguinte forma:

  1. A função process_imgs_and_save recebe o caminho para todas as imagens sintéticas e o acesso ao arquivo hdf5;
  2. Para cada imagem são extraídos os caracteres e as labels através da função segments_image_characters;
  3. A função segments_image_characters recebe os dados da imagem (Já em formato de array, lido através do opencv), o nome da imagem, e o valor de padding desejado para a imagem resultante da segmentação;
  4. A imagem é convertida para escala de cinza e binarizada para facilitar a detecção de cada caractere através de detecção de contornos (cv2.findContours);
  5. Os contornos detectados são ordenados em ordem crescente no eixo x, isto é, da esquerda para a direita na imagem;
  6. Através da posição dos contornos cada caractere é extraído da imagem, em conjunto com a label através da string de nome da imagem;
  7. É adicionada uma borda na cor branca por meio da função add_border para que a imagem de todos os caracteres possuam um tamanho fixo (80 de altura e 55 de largura). A imagem dos caracteres em conjunto com suas labels é retornada para a função process_imgs_and_save;
  8. Por fim os caracteres e as labels são adicionados ao buffer de escrita.

Abaixo está o código responsável pela segmentação dos caracteres e criação do arquivo hdf5.

Augmentação com a biblioteca albumentations

Antes de abordarmos o treinamento dos modelos, como as imagens dos caracteres gerados possuirão uma alta similaridade, devemos nos preocupar com a capacidade de generalização do modelo. Para isso, empregaremos a ferramenta Albumentations para modificar as imagens durante o treinamento.

Para instala-la, execute o comando abaixo:

pip install albumentations

Exemplo de augmentação

As augmentações que aplicaremos nas imagens serão:

  • Rotação;
  • Desfoque (Blur);
  • Mudança de perspectiva (Vista da imagem);
  • Modificações de brilho e contraste.

Cada augmentação tem uma probabilidade padrão de ocorrer, para altera-la basta configurarmos o parâmetro p. Ao ser executada a augmentação de alguma imagem, será retornadlo um dicionário no formato {"image" : imagem_numpy_array}

Para criar um objeto com todas as augmentações desejadas, utilizaremos o Compose.

Um exemplo de augmentação com os caracteres já no arquivo hdf5 pode ser executado, através do script augmentation_example.py:

Veja alguns exemplos de imagens geradas pelo script acima:

Imagem original / Imagem após augmentação
Imagem original / Imagem após augmentação
Imagem original / Imagem após augmentação
Imagem original / Imagem após augmentação

Interação Albumentations e Keras

Para integrar a ferramenta Albumentations com a etapa de treinamento do Keras, criaremos um gerador de imagens customizado.

Na definição da clase DataAugmentedGenerator herdamos a classe Sequence, dessa forma será instanciado um objeto que irá gerar as imagens de treinamento a cada batch. Para maiores informações, acesse a documentação do tensorflow.

Para construir um objeto gerador, devemos informar os seguintes parâmetros:

  • images: Array contendo os arrays das imagens de treinamento ou teste;
  • labels: Array contendo as labels das imagens de treinamento ou teste;
  • batch_size: Tamanho da cada batch;
  • augmentator: Objeto de augmentação, criado através do Compose.

Finalmente, os métodos __len__ e __getitem__ devem ser implementados. __len__ retorna a quantidade de batchs por época e __getitem__ retorna as imagens augmentadas a cada batch.

Treinamento dos modelos

Os códigos desta sessão podem ser encontrados no notebook ocr_train_with_synthetic_data.ipynb.

Para a etapa de treinamento, a abordagem foi treinar dois modelos, um para reconhecimento de números, outro para reconhecimento de letras.

Primeiro, efetuaremos a leitura do dataset:

É necessário deixar o arquivo hdf5 aberto, se não teremos problemas para acessar os datasets.

Agora, é necessário separar os dados em letras e números. Para isso utilizaremos as labels de cada imagem:

Com os índices dos números em mãos, selecionaremos as imagens de números e iremos separa-las em imagens para treino e teste. Para a separação dos dados adotaremos uma proporção de 80% para treino e 20% para teste.

Precisamos converter cada label para um vetor binário de classes. Para isso, utilizaremos o método keras.utils.to_categorical:

Com base na classe DataAugmentedGenerator instanciaremos um gerador de dados com augmentação para o treinamento e para o teste do modelo:

O modelo a ser treinado foi baseado na abordagem de redes convolucionais proposta na documentação do keras para o dataset MNIST. Como os valores de intensidade dos pixels variam entre 0 e 255, é uma boa prática normaliza-los para que possuam valores entre 0 e 1. Portanto,

incluiremos um layer lambda responsável por efetuar uma divisão por 255.0 (element-wise).

Treinaremos então o modelo utilizando os geradores de dados criados. Também é necessário salva-lo para uso posterior.

Os mesmos passos descritos acima foram utilizados para o treinamento do modelo para reconhecimento de letras. Exceto na quantidade de classes e na conversão de cada label para um vetor binário de classes, onde é necessário antes mapearmos cada letra para um valor numérico.

Abaixo temos os gráficos de acurácia e loss para os modelos treinados, onde é possível perceber que após a segunda época ambos convergiram para um valor ótimo. Isto pode ser explicado pela baixa variação entre nossos dados de treinamento e teste, mesmo com o processo de augmentação adotado.

Acurácia e Loss — Modelo para reconhecimento de números
Acurácia e Loss — Modelo para reconhecimento de letras

Resultados

Para o teste dos modelos foi efetuado o download de 3 imagens contendo placas de veículos. A posição das letras e números foi informada através de um arquivo json characters_position.json.

O processo adotado para teste dos modelos gerados foi:

  1. O arquivo json é lido, e extraído a posição das letras e números, assim como o caminho da imagem;
  2. Para cada imagem de placa, uma imagem das letras/números são extraídas;
  3. As imagens dos caracteres são passadas para o modelo classificar qual letra/número está presente na imagem;
  4. Por fim a área de cada caractere é desenhada na imagem, assim como a predição do modelo.

Os passos descritos acima, podem ser visualizados no script testing_model.py:

Abaixo temos as imagens, após a execução do script de teste:

Fonte: https://www.icarros.com.br/noticias/manutencao-e-servicos/sp:-licenciamento-para-placas-final-4-ja-comecou/21045.html
Fonte: https://draflaviaortega.jusbrasil.com.br/noticias/309061650/colocar-fita-adesiva-na-placa-de-veiculo-automotor-caracteriza-o-crime-do-art-311-do-codigo-penal
Fonte: https://www.carrodegaragem.com/como-localizar-veiculo-pela-placa/

Nas situações onde a placa estava bem alinhada, todos os caracteres foram corretamente classificados. Já na placa rotacionada, o primeiro caractere (F) foi classificado erroneamente como V.

Nossos próximos passos seriam entender o porque da classificação errônea e treinar um modelo mais preciso, seja através de uma augmentação mais agressiva, ou através de um fine-tunning da rede neural.

Vale comentar que neste caso de teste, a posição dos caracteres da placa foram indicados manualmente. Já em aplicações reais, a solução aqui proposta seria utilizada em conjunto com um detector de placas e alguma técnica de segmentação, responsável por extrair as posições das letras e números.

Considerações finais

Este artigo foi baseado em uma parte do meu trabalho de conclusão de curso de Especialização em Ciência dos Dados. Planejo escrever outros artigos, abordando a parte de detecção de placas e de segmentação dos caracteres. Toda e qualquer sugestão de melhoria é extremamente bem vinda!

Caso deseje me contatar, sintam-se a vontade para deixar um comentário aqui, ou me adicionar no Linkedin.

--

--

Felipe Crispim da Rocha Salvagnini
Data Hackers

A mechatronics engineer, falling in love with Machine Learning, computer vision, and technologies at all!! May the Force be with you!!