Extrair código de barras com Tesseract e OpenCV

Sobre esse assunto, encerro nesse artigo por uma razão bastante simples; esse tipo de interação demanda experimentação, e para cada emissor de boleto o processo deve ser diferente. Só me interessou escrever sobre o modo de extrair código de barras com Tesseract e OpenCV pela oportunidade de mostrar como poderia ser feito, mas não é a única maneira e pode nem ser uma das melhores. Explico.

Filtros do OpenCV

Na visão computacional temos um propósito específico, que é fazer o computador “enxergar” o que queremos. Mas para atingir esse objetivo, devemos tomar como base nossa própria seletividade. Por exemplo, olhe para a imagem a seguir e identifique uma circunferência:

A primeira forma destacada que vemos é um losango.  Dentro dele temos vários “ruídos”, que são as demais formas. Quando procuramos por uma circunferência, ignoramos tudo o que está à volta e nosso cérebro faz isso de uma maneira extremamente ágil, não precisamos de concentração nem esforço, apenas imaginamos uma circunferência e procuramos por essa referência na imagem. Olhe novamente para a mesma imagem e procure agora por uma estrela.

Até infringi a regra gramatical e fiz um novo parágrafo sendo que dei continuidade à ideia, mas a intenção é mostrar que o padrão solicitado não casa; não tem uma estrela na imagem acima. Quando utilizamos visão computacional fazemos a mesma coisa; abstraímos o todo em busca de um padrão. Quando escrevemos um código em OpenCV, definimos as regras para uma determinada característica que deve ser “vista” pelo “olho digital”. Mas e os filtros citados no título dessa explanação? Vamos lá.

O OpenCV possui um conjunto de filtros que se assemelham a um programa de edição de imagem. Só que quando usamos um programa de edição de imagem, queremos deixar a imagem nítida, desejamos harmonizar a composição visual, destacar cores. No OpenCV queremos somente destacar uma característica para que ela se torne um padrão que possa ser reconhecido.

Na imagem acima temos um círculo na metade direita superior do losango. Como temos apenas 1 incidência, é muito mais fácil casar o padrão. Mas se quiséssemos o menor retângulo vertical, qual seria? Nesse caso, temos um pouco mais de trabalho para identificar todos os retângulos, sendo que ainda assim podemos eventualmente deixar passar uma referência despercebida. O mesmo pode acontecer com OpenCV, mas não por engano, já que se trata de processamento computacional. Por essa razão, precisamos definir um filtro que não sofra influências de ruídos randômicos. Em imagens totalmente digitais ou que não sejam fotos de ambientes complexos, essa tarefa é simples. Vamos ver uma combinação de 2 filtros para separar a circunferência:

Canny

Esse filtro converte a imagem para binária. Como temos apenas duas cores, é bem fácil criar os limites lineares:

É mais ou menos o que nosso cérebro faz; quando sugeri procurar pela circunferência, as cores foram ignoradas e a busca foi feita pelo padrão da forma. Agora basta encontrar a circunferência. Os parâmetros usados em Canny foram:

sigma: 0,330000
minval: 100
maxval: 200
aperture: 3
normalization: default

 

Blob detector

Com esse filtro isolamos a área pretendida. Seu resultado não é imediato, pois diversas regiões serão identificadas e o filtro necessitará de ajustes, resultando em algo como:

A grande questão é a quantidade de ajustes para esses filtros. E não é só isso; seria possível utilizar filtros diferentes, existem diversas abordagens possíveis.

Para o blob detector foi usado o gatilho mínimo, máximo e passo, além do filtro por área, usando porcentagens da imagem com mínima e máxima.

mintrehshold: 10
maxthreshold: 14
tresholdstep: 10
minarea: 0,952500
maxarea: 16,000100

Nesse caso foi bastante simples e relativamente rápido fazer esse exemplo. A questão é que se a placa fosse um aviso de “limões no semáforo”, teríamos 3 círculos menores, além da cabeça. Esse limite definido no minthreshold seria o suficiente para pegar apenas a circunferência maior? Normalmente em uma imagem as ocorrências de um padrão são múltiplas, como no caso de boletos, que é praticamente constituído por retângulos que mantém a diagramação e apresentação visual. Outra questão é o tamanho do código de barras em relação à página; a posição do código de barras na página e a localização dos números digitáveis. Por essa razão, não tome esse exemplo como um caso de uso genérico, ele é específico para o boleto da Vivo, apenas para mostrar como se referenciar por um padrão para pegar outra região.

Exemplo de leitura dos dígitos

Já mostrei nesse outro artigo como ler um PDF que não seja composto exclusivamente por imagem, utilizando expressão regular para filtrar os números. Já mostrei outro modo de criar delimitadores nesse outro artigo. O problema de se fazer um processo semelhante passando uma imagem para o Tesseract é que a influência do texto periférico pode influenciar na extração dos números. Pra evitar erros, é melhor extrair a região específica e posteriormente aplicar o OCR.

Blur

Escolhi primeiramente borrar a imagem para criar regiões, já que o arquivo é composto majoritariamente por texto. Os retângulos pretos são para garantir a segurança da informação pessoal, o resto do documento é padrão.

Clahe

O próximo filtro é o que destaca o conteúdo. Mais uma vez, essa combinação de filtros foi uma escolha pessoal e ainda assim conversei com o Leonardo Lontra (um dos desenvolvedores que criam recursos para o OpenCV) para ver se não estava muito fora do ideal.

extrair código de barras - clahe

Não parece ter mudado muito além de escurecer, mas acredite, mudou sim.

Mser

Para detectar as regiões, utilizei o Mser.

Aqui também omiti algumas partes do dígito do código, além do próprio código de barras. Tendo isolado a região do código de barras, basta agora extrair a área de interesse. Nem precisa ser somente os números:

Depois, basta passar para o Tesseract, seja pelo Python ou pelo Shell (claro que por Python fica um código limpo, mas vamos lá):

Código para extrair código de barras

Bem, a questão é que só vai funcionar para esse modelo de boleto. Tenho outro modelo aqui da Vivo, que necessitará outro tratamento.

Devo lembrar também que esse código por si só não representa nada em um projeto, extrair código de barras de um arquivo é apenas a ponta do iceberg.

Lembre-se de que o arquivo pdf possui páginas, será necessário extrair a página de interesse. Fora isso, convertê-la para imagem. Leia esse artigo para as devidas conversões.

import cv2 as cv
import sys

if not sys.argv[1]:
    exit(0)

features = {}
image    = cv.imread(sys.argv[1])

#Blur
#Clahe
#Mser

def add_info(name,info):
    if name not in features.keys():
        features[name] = []
        features[name].append(info)

def apply(im):
    if not len(im.shape) == 2:
        im = cv2.cvtColor(im, cv2.COLOR_BGR2LAB)
        im[:, :, 0] = clahe.apply(im[:, :, 0])
        im = cv2.cvtColor(im, cv2.COLOR_LAB2BGR)

    imsize = im.shape[0] * im.shape[1]
    minArea = 1.0
    maxArea = 13.4651
    minArea = (float(minArea) / 100.0) * float(imsize)
    maxArea = (float(maxArea) / 100.0) * float(imsize)

    mser.setMinArea(int(minArea))
    mser.setMaxArea(int(maxArea))
    mser.setPass2Only(True)

    (regions, bboxes) = mser.detectRegions(im)


def compose():
    #---------------  Blur -------------------
    dtype      = image.dtype
    blur_w     = 16
    blur_h     = 1
    blur_r     = 1
    borderType = getattr(cv,'BORDER_DEFAULT')

    blur_w     = 1 + 2 * blur_w
    blur_h     = 1 + 2 * blur_h
    blur_r     = (blur_w, blur_h)

    blur       = cv.blur(image,blur_r,borderType=borderType).astype(dtype)
    cv.imwrite("blur.jpg",blur)

    #------------------------- Clahe --------------------------------

    blur           = cv.cvtColor(blur, cv.COLOR_BGR2GRAY)

    clahe          = cv.createCLAHE()
    clipLimit      = 30.0
    tileGridSize_w = 1
    tileGridSize_h = 1

    clahe.setClipLimit(clipLimit)
    clahe.setTilesGridSize((tileGridSize_w,tileGridSize_h))

    clahe_img = clahe.apply(blur)
    cv.imwrite('clahe.jpg', clahe_img)

    #-------------------------- Mser --------------------------------
    visual_feedback = True
    
    minArea         = 1.0
    maxArea         = 13.4651
    max_variation   = 0.25
    min_diversity   = 0.1
    max_evolution   = 20
    area_threshold  = 1.01 
    min_margin      = 0.003
    blur_size       = 2

    mser      = cv.MSER_create(_delta=1,_max_variation=0.25,
                                        _min_diversity=0.1,
                                        _max_evolution=20,
                                        _area_threshold=1.01,
                                        _min_margin=0.003,
                                        _edge_blur_size=1 + 2 *2)

    if len(clahe_img.shape) == 2:
        im = image #cv.cvtColor(clahe_img, cv.COLOR_BGR2LAB)
        im[:, :, 0] = clahe.apply(im[:, :, 0])
        im = cv.cvtColor(im, cv.COLOR_LAB2BGR)


    imsize  = clahe_img.shape[0] * clahe_img.shape[1]
    minArea = (float(minArea) / 100.0) * float(imsize)
    maxArea = (float(maxArea) / 100.0) * float(imsize)

    mser.setMinArea(int(minArea))
    mser.setMaxArea(int(maxArea))

    mser.setPass2Only(True)

    (regions, bboxes) = mser.detectRegions(clahe_img)

    if bboxes is not None:
            for bb in bboxes:
                x, y, w, h = bb
                add_info('bboxes', bb)

                if not visual_feedback: continue
                if clahe_img.ndim == 2:
                    mser = cv.cvtColor(clahe_img, cv.COLOR_GRAY2BGR)
                if w-x > 600:
                    print("detected")
                    print(x)
                    print(y)
                    print(w)

                    print(x)
                    cv.rectangle(image, (x, y), (x+w, y+h), (0,255,0), 5, cv.LINE_AA)
                    cropped = image[y:y+h, x+200:x+w-200]
                    print(bboxes)
                    cv.imwrite('barcode.jpg',cropped)

            cv.imwrite('mser.jpg', image)

            
compose()

Pra finalizar o artigo, não dou consultoria. Por favor, não me procure para perguntar nada, para fazer “um ajuste simples”, para “escrever um exemplo rápido”  nem nada que não envolva remuneração. É chato falar assim, mas conforme envelheço, a paciência diminui, o tempo se torna mais escasso e convenhamos; estamos em tempos difíceis.

Documentação do OpenCV

Apenas para constar (porque o ideal é começar por tutoriais), esse é o link da documentação oficial do OpenCV.

 

Revisão: Ricardo Amaral de Andrade

Djames Suhanko

Djames Suhanko é Perito Forense Digital. Já atuou com deployer em sistemas de missão critica em diversos países pelo mundão. Programador Shell, Python, C, C++ e Qt, tendo contato com embarcados ( ora profissionalmente, ora por lazer ) desde 2009.