Existem alguns poucos acrônimos para reconhecimento de placas de carro como o LPR, ANPR (Automatic Number Plate Recognition), AVI (Automatic Vehicle Identification) e APD (Automatic Plate Detection, que é a primeira fase).
Bem, aqui se somam diversas ferramentas para chegar ao objetivo e ao final, esse recurso poderia ser utilizado para algo mais além de dar multas; poderia abrir o portão de sua casa automaticamente, com uma segunda chave de segurança como o beacon; Chegou a pé, o portão não se abre. Chegou de carro com o beacon, o reconhecimento da placa é feito e então o portão aberto. E muitas outras utilidades, como determinar se o morador do condominio se dirigiu corretamente à sua vaga (fazendo tracking após tê-lo reconhecido pela placa).
As demais aplicações e enlaces com IoT ficarão por sua conta. Nesse tutorial pretendo apenas deixar de presente o código para você integrar em seu sistema de forma livre e gratuita.
Fases do reconhecimento
Quando se está fazendo reconhecimento facial, apenas o face detection precede a fase do face recognition. Não que a tarefa seja mais fácil, mas para o LPR o conjunto de ferramentas difere. A arte agora é definir o quê deverá ser empregado para obter um bom resultado. Primeiro, vamos considerar as variáveis de ambiente.
Luz
A luz sempre variará mas para a detecção de placa isso é praticamente irrelevante. Porém, para fazer a leitura da placa à noite, será necessário um controle extra, devido ao alto nível de reflexibilidade. Por enquanto, vamos considerar o melhor dos mundos.
ângulo
A câmera só está de frente com a placa do carro em casos de portaria e pedágio. Quando a câmera está na rua, seja radar ou comércio, para identificar a placa o processo pode ser um pouco mais trabalhoso devido ao ângulo da câmera em relação ao carro ou a inclinação do carro em relação à câmera.
Gatilho
Não é viável correr todo um processo sobre um frame se podem existir intervalos de segundos; considerando 10 frames por segundo, imagine a quantidade de processamento disperdiçado e o possível delay que isso poderia gerar. Para evitar esse tipo de problema, a melhor opção é utilizar um gatilho para que o processo só ocorra quando disparado. Isso pode ser feito de forma mecânica, eletrônica ou digital, criando uma cerca virtual.
ROI
O processamento sobre um frame pode se tornar uma tarefa complexa, dependendo principalmente da resolução. Considerando que você tenha uma câmera em um lugar alto, obviamente a resolução de captura precisará ser maior ou os detalhes se perderão. Por exemplo, você pode seguramente utilizar full HD na câmera para ter uma área imensa de pixels , o que certamente fará o objeto distante se tornar mais próximo para a análise. Porém, analisar um frame desse tamanho pode ser a morte para o sistema, então aliado ao gatilho (que também é um ROI), uma região de interesse (ROI) extraida do frame de trabalho pode ser a salvação nesse caso.
Juntando tudo
Então o fluxo do programa para o reconhecimento de placas deve ser:
- Gatilho – onde um evento é gerado quando uma região linear for cruzada.
- Recorte – uma cópia da região da imagem principal é repassada adiante.
- Plate Detection – Escaneamento da região copiada para separar a placa.
- Plate Recognition – A conversão da placa em caracteres interpretáveis.
Se falhar o plate detection, o plate recognition nem precisa ser executado. Isso é importante, porque processamento poderá ser economizado em casos de falso-positivo.
O melhores resultados podem ser obtidos através de uma câmera com IR, pela clareza oferecida na imagem, uma vez que a iluminação por IR se sobressai com o reflexo da placa, que reflete toda a luz na área branca e absorve a luz no preto.
Essa imagem abaixo é de um anti-radar, por isso que faltam partes na placa, mas é assim que sua placa é vista no comércio de multas de São Paulo:

Características da placa
Todos, exceto moto



O Brasil já passou por uma sacolada de tipos de placa. De 2000 em diante, ainda algumas outras mudanças aconteceram e a atual placa tem o seguinte padrão:
- Altura de 130mm
- Largura de 400mm
- Caracteres na altura de 53mm (tem outra resolução mais antiga que diz 63mm)
- Fonte Mandatory
- Largura do traço de 10mm
Apenas moto



As motos possuem um formato mais compacto, onde as letras ficam em cima e os números embaixo. Seu padrão:
- Altura de 170mm
- Largura de 200mm
- Caracteres na altura de 53mm
- Fonte Mandatory
- Largura do traço de 6mm
Atualmente a placa é branca refletiva com o nome da cidade, mas já estão previstas as mudanças para a próxima placa no padrão Mercosul, onde o topo da placa recebe o nome do país. Essa não será a única mudança, porque o país ficará sobre uma faixa azul de lado a lado da placa. Pra piorar, não haverá mais o padrão letra/número, mas será uma sequência aleatória de 7 caracteres alfanuméricos. Também haverá desenho da bandeira do Brasil, bandeira da federação e brasão do municipio com nome por extenso. Só vai faltar o escudo do seu time de futebol.
A cor da borda da placa e a cor dos caracteres variarão conforme a categoria do veículo. A fonte será a FE Engschrift, já adotada em placas na alemanhã. A fonte é coisa linda:



E a placa ficará essa belezura:



Download das fontes
Se quiser as fontes pra fazer seus .jpg para teste, você pode baixar a Mandatory nesse link e a FE-Font nesse link.
Algorítmo de reconhecimento da placa
Como dito anteriormente na sequência de passos, devemos primeiro desenvolver uma classe de detecção, depois uma de reconhecimento. Vamos manter o foco nisso por enquanto porque é possível que seu interesse seja exclusivamente nessas duas partes, mas provavelmente em outro artigo detalho os passos precedentes à detecção e reconhecimento.
A lógica do fluxo é bastante simples.
- Inicio do programa
- Localizar placa no frame atual
- SE não existe, encerra o passo atual do loop aqui. SENAO
- Faz o reconhecimento da placa
- Exibe o resultado
No código a coisa é um pouco mais complicada. Isso porque entram novas técnicas que não são necessárias no face detection/recognition (por exemplo), que é a segmentação das áreas que compõe a placa. Eu, como novato na área, posso dizer que para mim não foi nada simples chegar nesse resultado, ainda que tendo comprado alguns livros sobre OpenCV e um sobre redes neurais. Como dizem, “no pain, no gain”.
Plate detection
Se você não está aprendendo também visão computacional e redes neurais, nesse ponto você já deve ter em mente que a visão computacional trabalha sobre cada frame recebido em um loop, como se fosse uma imagem .jpg (a extensão é para exemplificar) sendo lida por um editor de imagens. Você deve acomodar esse conjunto de dados em uma variável e trabalhar em uma cópia dela. Nessa cópia convertida em grayscale passamos um algorítmo que irá fazer a primeira análise e separar a placa.
O primeiro passo é a segmentação para validar um conjunto de características que definam uma região como sendo uma placa de carro. Esse passo é um pouco duro, porque exige uma série de filtros e o conjunto de resultados deve decidir se aquilo é ou não uma placa.
O segundo passo é a classificação. Para reforçar o resultado, utilizaremos um SVM sobre a imagem. Aqui o trabalho é ainda mais intenso.
Um ponto importantíssimo a considerar é a distância entre a câmera e a placa, por isso você deverá definir um gatilho. Porém o carro em movimento pode diferir a distância conforme sua velocidade. Veremos mais detalhes, mas por ora vamos pensar na situação mais simples, que é o carro servindo-nos de modelo fotográfico.
Segmentação
A tarefa aqui é particionar a imagem de forma que se torne simples a análise e extração de dados. Uma das mais importantes características na segmentação da placa do carro é o número elevado de vertices verticais quando vista de forma frontal. Além, disso, não deverá haver rotação ou distorção de perspectiva, mas calma, no final a tolerância será maior.
Como quase sempre, devemos trabalhar na imagem em grayscale e remover os ruídos de câmera e ambiente. Um conjunto de filtros pode ser aplicado como a contração e dilatação subsequente para eliminar minimamente pontos extras. Além disso, um gaussian blur – ou apenas ele, de modo que a melhor opção é criar 3 métodos para aplicar os filtros por experimentação e utilizar apenas os que interessam no final.
O OpenCV possui 3 filtros de passa-alta: Sobel, Scharr e Laplacian.
As operações do filtro de Sobel somam o gaussian smoothing com a operação de diferenciação, o que o torna mais resistente a ruído. A direção da derivativa pode ser dado pelo argumento X ou Y. O kernel size também é customizável. Se o kernel for 3×3, o Scharr pode ser mais adequado mas enfim, experimentação é a chave.
Veja meu teste inicial que fiz baseado em um exemplo que não tinha relação direta com esse artigo, mas era especificamente sobre filtros:



O mesmo para uma placa já com um zoom:



A simples adição de blur já deu uma melhorada significativa porque ajuda a reduzir o ruído, por isso não existe um padrão, você deve criar sua combinação de filtros para chegar no melhor resultado. Até esse ponto temos:



Threshold
A determinação dos limitrofes são feitas nesse ponto sobre o processamento prévio da imagem. Isto é, após removido o ruído e aplicado o filtro de Sobel, aplicam-se os thresholds. Utilizando o THRESH_OTSU, a função retorna o melhor valor obtido pelo algorítmo de Otsu. Essa operação como está sendo feita também é conhecida como “binarização”.
Para ter uma noção, os thresholds tem essas funções:



A operação foi feita sobre o Sobel de X, resultando nisso até agora:



Operação morfológica
Com a operação morfológica removemos espaços em branco entre cada linha vertical e conectamos todas as regiões que possuem um número significativo de bordas. Nesse passo teremos a possível região contendo a placa do carro.
Visualmente o resultado não parece nada bom, mas é como a arte de pintar um quadro; do borrões iniciais surgem as obras de arte no fim, tranquilize-se e tenha em mente que na detecção o que queremos é apontar ao próximo passo onde está localizada a placa no frame.



Com um pequeno ajuste, toda a região da placa ficará interligada, formando assim o retângulo que necessitamos para a detecção da região da placa. Mas supondo que fosse um caso onde não houvesse ainda uma condição de interligar essas regiões; utilizando contornos você pode fazer a marcação necessária para a conexão através da espessura do traço.
Encontrando contornos
Conforme a documentação do OpenCV, os contornos podem ser explicados como uma junção contínua de pontos em torno de uma região contendo a mesma cor ou intensidade. São principalmente úteis para detecção de uma região para detecção.
A documentação sugere que seja utilizada imagem binária, isto é, preto e branco; confere.
A operação de contorno altera a imagem alvo, portanto tenha certeza de que ela poderá ser descartada trabalhando em uma copia derivada.
Com um pequeno ajuste, não houve a necessidade da utilização de cntornos. De qualquer modo, exponho a imagem para que você veja o resultado:



Como esse artigo já se extendeu demais, vou deixar a finalização para a “Parte II” desse artigo, mas se quiser dar uma “cercada” na área, aproveite esse artigo sobre a detecção de retângulos para brincar. E não se preocupe, não vamos utilizar esse OCR simples desse outro artigo para fazer a leitura.
O código de experimentação é esse a seguir; pode ser que mude tudo ou mude nada. Mais uma vez, acompanhe no próximo artigo.
import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread('placaFoco.jpg',0) newImg = cv2.blur(img,(5,5)) img = newImg laplacian = cv2.Laplacian(img,cv2.CV_64F) sobelx = cv2.Sobel(img,cv2.CV_8U,1,0,ksize=3,scale=1,delta=0,borderType=cv2.BORDER_DEFAULT) sobely = cv2.Sobel(img,cv2.CV_8U,0,1,ksize=3,scale=1,delta=0,borderType=cv2.BORDER_DEFAULT) #aplicado o threshold sobre o Sobel de X tmp, imgThs = cv2.threshold(sobelx,0,255,cv2.THRESH_OTSU+cv2.THRESH_BINARY) #pequena chacoalhada nos pixels pra ver o que cai (isso limpa a img mas #distancia as regioes, experimente) #krl = np.ones((6,6),np.uint8) #erosion = cv2.erode(imgThs,krl,iterations = 1) #krl = np.ones((19,19),np.uint8) #dilation = cv2.dilate(erosion,krl,iterations = 1) #imgThs = dilation #estrutura proporcional aa placa morph = cv2.getStructuringElement(cv2.MORPH_RECT,(40,13)) #captura das regioes que possam conter a placa plateDetect = cv2.morphologyEx(imgThs,cv2.MORPH_CLOSE,morph) regionPlate = plateDetect.copy() contours, hierarchy = cv2.findContours(regionPlate,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE) cv2.drawContours(regionPlate,contours,-1,(255,255,255),18) plt.subplot(3,3,1) plt.imshow(img,cmap = 'gray') plt.title('Original') plt.xticks([]) plt.yticks([]) plt.subplot(3,3,2) plt.imshow(laplacian,cmap = 'gray') plt.title('Laplacian') plt.xticks([]), plt.yticks([]) plt.subplot(3,3,3) plt.imshow(sobelx,cmap = 'gray') plt.title('Sobel X') plt.xticks([]), plt.yticks([]) plt.subplot(3,3,4) plt.imshow(sobely,cmap = 'gray') plt.title('Sobel Y') plt.xticks([]), plt.yticks([]) plt.subplot(3,3,5) plt.imshow(imgThs,cmap = 'gray') plt.title('Threshold') plt.xticks([]), plt.yticks([]) plt.subplot(3,3,6) plt.imshow(plateDetect,cmap = 'gray') plt.title('Morphology') plt.xticks([]), plt.yticks([]) plt.subplot(3,3,7) plt.imshow(regionPlate,cmap = 'gray') plt.title('Draw Contours') plt.xticks([]), plt.yticks([]) plt.show()
Se quiser utilizar a mesma imagem, eis:



Esse artigo tem a parte 2, que deve aparecer um pouco mais abaixo, nos artigos sugeridos. O OCR do texto fiz no modo “trapaça” pra não ter que implementar uma rede neural eu mesmo, já que não tenho propósito pra esse projeto. Como certamente quererá fazê-lo, o artigo é esse (usando Tesseract com LSTM).
Excelente artigo, parabéns!
Como posso entrar em contato direto?
Amigo minhas cameras de alta resolução a noite nao pegam placa. Fica tipo o brilho do anti radar. Consigo pegar uma camera destas e modificar o firmware para ficar como as cameras LPR que fazem excelente leitura de placa a noite?
voce precisa de uma camera dedicada, entao baixar o contraste até a placa ficar visivel e isolar a placa apenas. com o contraste baixo, vai ser fácil separar a placa.