OpenCV + Microsoft OCR (reconhecimento de caracteres em imagens)
Computer vision Raiz + Nutella = Projeto entregue!
Nem sempre temos tempo para ficar reinventando a roda, por mais que seja legal você ter seus próprios modelos e seus próprios treinamentos voltados pro teu problema, temos prazo de entrega, e muitos das aplicações que construirmos ou vamos construir podem necessitar de um OCR.
O que é um OCR?
Optical Character Recognition ou Reconhecimento Óptico de Caracteres, basicamente é uma tecnologia de reconhecimento de caracteres através de PDFs, Imagens(papel escaneado, foto tirada do celular, e etc), videos(imagens) e outras fontes.
Através dessas mídias, tu não consegue manipular um texto de modo convencional (como um editor de texto), seja para inputar texto, alterar, deletar… para isso, primeiro você precisa extrair o texto contido na imagem/pdf/video, e é exatamente isso que o OCR faz.
O que é o OpenCV?
O OpenCV é de longe a biblioteca a mais popular biblioteca de visão computacional, você pode trabalhar com ela de uma forma mais alto nível(usando bastante métodos “mágicos”) ou baixo nível (trabalhando com cálculos matriciais).
Problema
Digamos que temos fotos de documentos e queremos renomeá-los com a data contida dentro da imagem.
Ao lado esquerdo temos um documento de exemplo ao qual vamos trabalhar em nossa demo, note que nele há uma data (27 September 2005), e é exatamente essa data que nós vamos pegar para poder renomear nosso arquivo(a própria imagem).
Beleza, temos duas opções:
1 — Abrimos imagem por imagem, lemos a data contida nela e depois renomearmos o arquivo
2 — Montamos um script que faça isso para nós.
Vamos seguir pelo caminho 2
Show me the code, baby
obs: O código estará disponibilizado no github.
Instalando bibliotecas
Começando pela nossa biblioteca de visão computacional, vamos instala-la com o pip.
pip install opencv-python
Importando as libs
cv2 — Apresento-lhe o OpenCV, importamos essa lib com o cv2
glob — Lib que nos possibilita encontrar arquivos dentro de pastas de forma recursiva, ou seja, você da um path e o nome de um aquivo pra ele, e ele procura esse arquivo por dentro de todas as pastas que existe no diretório original(path)
[Listando todas as fotos de um diretório]
Lembrando que o nosso problema é renomear todos as imagens de uma determinada pasta pela data contida no documento(imagem).
Aqui eu tenho uma pasta cheia de imagens de documentos
Aqui estou trabalhando com o mesmo documentos em todas as fotos, porém em um caso real, poderia ser documentos diferentes.
Vamos iterar sobre essas imagens e então para cada imagem, nós vamos obter a data contida nela.
Para listar e iterar sobre todos os arquivos contidos dentro de um diretório, nós vamos usar o glob.
Dentro de path, vamos ter o caminho completo das nossas imagens(note que só estou listando arquivos .jpg)
Para cada imagem, nós vamos:
- Carrega-lá na memória;
- Transforma-la em preto e branco;
- Recortar a area da data;
- Fazer a chamada para o serviço cognitivo da Microsoft.
Lendo uma imagem
Para ler uma Imagem com o OpenCV nós vamos utilizar o método .imread() e vamos passar como parâmetro o caminho (relativo ou não) da nossa imagem.
O método .imread() nos retornará um valor do tipo <class ‘numpy.ndarray’>, uma classe numpy, e se imprimirmos esse valor, veremos que nossa imagem é basicamente um array de arrays, ou seja, uma matriz.
[[[255 255 255]
[255 255 255]
[255 255 255]
...
[255 255 255]
[255 255 255]
[255 255 255]]
E o que significa esses 255 dentro dessa matriz, essas são as cores BGR da imagem, que variam de 0 à 255, como a imagem que nós temos só tem duas cores (preta e branco) nós só vamos ter valores 0 (ou muito perto de 0) ou 255(ou muito perto de 255).
Caso você nunca tenha ouvido falar sobre BGR, entenda-se que é um RGB com uma outra ordem (Blue, Green, Red), tem um motivo para isso, mas como não é o foco do artigo, não irei adentrar nesse assunto.
Mas qual o tamanho dessa matriz? podemos obter essa resposta com a propriedade .shape
print(image.shape)
> (1755, 1104, 3)
Note que temos 3 valores, altura, largura e profundidade), a altura da nossa matriz aqui se refere a altura da na nossa imagem em pixels, e a mesma coisa para a largura. Quanto à profundidade, isso são os canais da sua imagem, (Blue, Green, Red).
Imprimindo uma imagem
Caso você queria ver a imagem ao qual você está trabalhando basta chamarmos o famoso matplotlib
_, ax1 = plt.subplots(figsize=(20,10))
ax1.imshow(image)
Mas para trabalhar com essa matriz toda, uma matriz de 1755x1104x3 = 5812560, vai demandar muito da sua máquina, mas podemos usar uma técnica que irá diminuir a quantidade de pixels a se trabalha, basta transformar a nossa imagem para cores cinzas, onde nós teremos apenas 1 matriz com pixels 0 zero a 255.
[Convertendo uma imagem BRG para GRAY(coloração cinza)]
Para fazer isso, vamos usar o método .cvtColor(), ele recebe 2 parametros, uma imagem(matriz numpy) e um tipo de cor, existem vária, BGR2GRAY, COLOR_BGR2XYZ, COLOR_BGR2YCrCb e assim vai. Esse método nos retornará uma nova imagem(matriz numpy) com uma imagem de coloração P&B(preto e branco)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
print(gray.shape)
E se nós printarmos as dimensões dessa imagem, veremos que agora temos apenas 2 dimensões, X e Y, não temos mais 3 canais como antes.
Se imprimirmos essa imagem, não veremos diferença pois originalmente essa imagem já era preto e branco, porém no esquema de cor BRG, agora ela permanece preto e branco, porém no esquema GRAY, se você executar esse mesmo processo para uma imagem colorida e imprimi-la, verá que a imagem será impressa em preto e branco.
_, ax1 = plt.subplots(figsize=(20,10))
ax1.imshow(gray)
Recortando uma imagem
Agora que temos menos pixels para trabalhar, vamos fazer um recorte na nossa imagem, esse recorte vai servir para que enviamos apenas a data pro nosso OCR, no caso do nosso problema, todos os documentos tem o mesmo formato, imagine que são contratos e que a data do documento sempre fica no mesmo lugar.
Pra isso, nós vamos fazer o seguinte, vamos definir alguns pontos de recorte (em pixels)
t_y,b_y,l_x = 310, 380, 490
- t_y — top y, é a altura do corte de cima;
- b_y — bottom y, é a altura do corte de baixo;
- l_x — left x, é o pixel que vamos cortar no eixo X, na “largura”
Não estou listando um “r_x” pois eu vou deixar com o limite máximo pro corte no lado direito
E então vamos cortar nossa matriz numpy
image = image[t_y:b_y,l_x:]
Na nossa imagem(numpy), pegamos apenas os indices que contemplam o pedaço que queremos da nossa imagem, e no fim vamos ter esse resultado.
OCR
Agora que temos nossa data recortada, vamos manda-la para o OCR da Microsoft.
Na página oficial, você pode fazer alguns teste no serviço deles antes mesmo de codificar algo pra consumir a api deles.
Se descermos até o meio da página, veremos uma area para enviarmos uma imagem(ou usar a URL dela) e ao lado, poderemos ver o resultado textual dessa imagem.
Enviado a imagem, ao lado direito você verá as palavras encontrada e uma opção de JSON, onde você pode ver essas palavras em um estrutura lógica.
Dentro desse JSON, teremos o recognitionResult
que seria os resultados obtidos pelo OCR da MS, e dentro dele vamos ter as linhas, e dentro de cada linha identificada temos o seguinte objeto:
{
"boundingBox": [
70,
59,
588,
50,
589,
114,
71,
122
],
"text": "27 September 2005",
"words": [
{
"boundingBox": [
73,
60,
141,
62,
141,
119,
72,
118
],
"text": "27"
},
{
"boundingBox": [
164,
62,
445,
60,
445,
116,
163,
120
],
"text": "September"
},
{
"boundingBox": [
468,
59,
590,
53,
590,
109,
468,
115
],
"text": "2005"
}
]
Note que temos as palavras de forma separada em recognitionResul.lines[x].text
e também o texto completo recognitionResul.lines[x].words[y].text
No final da página, você encontrará um botão para testear o serviço
Após clicar nele é só fazer o cadastro e obter sua KEY
Você terá um prazo de 7 dias 4free pra testar a API.
Após fazer o seu cadastro, você irá receber sua key
Você terá uma key para a api v1 e api v2, guarda-as, pois vamos precisar delas mais tarde.
Voltando para o código
Para fazermos nossa request, vamos definir algumas coisas, como:
i — Como o glob não nos da o índice do nosso path, vamos trabalhar da forma mais simples possível para esse artigo, vamos ter uma variável de controle que será o nosso contador;
Buffer Image — Precisamos transformar nossa imagem que originalmente é um array numpy para um buffer (array de bytes), pois é isso que vamos enviar apar o OCR da MS;
KEY — A chave que você pegou na página da microsoft;
VisionBaseURL — O serviço de visão computacional da Microsoft tem vários serviços, e todos eles você acessa da mesma url, apenas muda a rota no final, no nosso caso, será “ocr”
ContentType — é o tipo de request que vamos fazer, no nosso caso, vamos fazer um stream, enviando um buffer de dados da nossa foto.
DetectOrientation — Setamos essa flag pata indicar para o OCR corrigir/virar a orientação das imagens.
E então vamos fazer nossa request de fato
Dentro de analysis, terá o json ao qual vamos obter os nomes contidos na imagem que enviamos para o OCR.
Se imprimirmos essa variável, veremos algo como:
print(analysis)
> {'language': 'en', 'textAngle': 0.0, 'orientation': 'Up', 'regions': [{'boundingBox': '29,31,277,29', 'lines': [{'boundingBox': '29,31,277,29', 'words': [{'boundingBox': '29,33,29,20', 'text': '27'}, {'boundingBox': '77,32,151,28', 'text': 'September'}, {'boundingBox': '246,31,60,21', 'text': '2005'}]}]}]}
Note que temos uma key chamada language, esse cara não foi detectado automaticamente, é você quem passa esse parâmetro(dentro de options)na hora de fazer a request.
Trabalhando os dados
Nesse momento, o que queremos é o texto completo, para pegar essa informação, temos que passar por todas “words” e pegar as palavras encontradas na imagem.
Se você imprimir “texts”, verá algo como:
print(texts)
> ['27', 'September', '2005']
Daí é só nos incrementarmos nosso contador, concatenarmos isso tudo em uma string só e renomear nosso arquivo(ou no nosso caso, salvar em uma pasta diferente)
Se visualizarmos nossa pasta, todos os arquivos estarão renomeados com a sua respectiva data.
Caso você queira, você pode converter esse tipo de data para algo como dd/MM/yy usando o time.strtime .