Extrair informações de texto de arquivos PDF com diferentes layouts - aprendizado de máquina

8

Preciso de assistência com um projeto de ML que atualmente estou tentando criar.

Recebo muitas faturas de vários fornecedores diferentes - tudo em um layout exclusivo. Preciso extrair três elementos-chave das faturas. Esses três elementos estão todos localizados em uma tabela / itens de linha para todas as faturas.

Os 3 elementos são:

  • 1 : Número da tarifa (dígito)
  • 2 : Quantidade (sempre um dígito)
  • 3 : valor total da linha (valor monetário)

Consulte a captura de tela abaixo, onde eu marquei esses campos em uma amostra da fatura.

insira a descrição da imagem aqui

Comecei este projeto com uma abordagem de modelo, com base em expressões regulares . Isso, no entanto, não era escalável e acabei com toneladas de regras diferentes.

Espero que o aprendizado de máquina possa me ajudar aqui - ou talvez uma solução híbrida?

O denominador comum

Em todas as minhas faturas, apesar dos diferentes layouts, cada item de linha sempre será composto por um número de tarifa . Esse número de tarifa é sempre de 8 dígitos e é sempre formatado da seguinte maneira:

  • xxxxxxxx
  • xxxx.xxxx
  • xx.xx.xx.xx

(Onde "x" é um dígito de 0 a 9).

Além disso , como você pode ver na fatura, existe um preço unitário e um valor total por linha. A quantidade que precisarei é sempre a mais alta para cada linha.

A saída

Para cada fatura como a acima, preciso da saída de cada linha. Por exemplo, isso pode ser algo como isto:

{
    "line":"0",
    "tariff":"85444290",
    "quantity":"3",
    "amount":"258.93"
},
{
    "line":"1",
    "tariff":"85444290",
    "quantity":"4",
    "amount":"548.32"
},
{
    "line":"2",
    "tariff":"76109090",
    "quantity":"5",
    "amount":"412.30"
}

Para onde ir daqui?

Não sei ao certo o que estou procurando fazer se enquadra no aprendizado de máquina e, em caso afirmativo, em qual categoria. É visão computacional? PNL? Reconhecimento de entidade nomeada?

Meu pensamento inicial era:

  1. Converta a fatura em texto. (As faturas são todas em PDFs com texto, para que eu possa usar algo como pdftotextpara obter os valores textuais exatos)
  2. Criar personalizado entidades nomeadas para quantity, tariffeamount
  3. Exporte as entidades encontradas.

No entanto, sinto que posso estar perdendo alguma coisa.

Alguém pode me ajudar na direção certa?

Editar:

Veja abaixo mais alguns exemplos de como pode ser uma seção da tabela de faturas:

Modelo de fatura nº 2 insira a descrição da imagem aqui

Modelo de fatura nº 3 insira a descrição da imagem aqui

Edição 2:

Veja abaixo as três imagens de exemplo, sem as caixas de bordas / bordas:

Imagem 1: Amostra 1 sem bbox

Imagem 2: Amostra 2 sem bbox

Imagem 3: Amostra 3 sem bbox

oliverbj
fonte
Você pode mostrar mais alguns exemplos dos PDFs de entrada para ver quanta variação realmente existe? (= quão flexível será a solução)
sjaustirni
@sjaustirni Acabei de adicionar mais dois! Acredito que a maior variação entre as faturas de fornecedor é como o layout da tabela é (e subsequentemente os itens de linha e como o texto específico é formatado).
oliverbj 07/02
Perfeito! Dado esses exemplos, eu provavelmente converteria o pdf em texto e tentaria emparelhar os valores nele com o rótulo anterior (por exemplo, Tariff No.:ou $) ou a coluna à qual ele pertence (aqui pode ajudar a salvar as informações espaciais das letras, se alguma ferramenta de OCR fizer isso). Eu acredito que você não precisa entrar no aprendizado de máquina com esse problema (além do OCR pré-fabricado), nem da PNL (não é uma linguagem natural). No entanto, sem ver como essas ferramentas funcionam com seus dados, podemos apenas especular qual é o próximo passo e o que é necessário: D
sjaustirni
@sjaustirni não terminaria da mesma maneira que eu já estou fazendo, o que não é escalável? (Abordagem baseada em modelo / regex).
oliverbj 7/02
Você não pode extrair a própria tabela do pdf para uma estrutura de dados e depois processar as colunas? Pode ser que você possa usar tabula-py para fazer isso e, em seguida, obter diretamente a quantidade e o total, e com alguma regex, a tarifa
dhanushka

Respostas:

4

Estou trabalhando em um problema semelhante no setor de logística e confie em mim quando digo que essas tabelas de documentos vêm em diversos layouts. Inúmeras empresas que resolveram um pouco e estão melhorando esse problema são mencionadas como

  • Líderes: ABBYY, AntWorks, Kofax e WorkFusion
  • Principais concorrentes: Automação em qualquer lugar, Celaton, Datamatics, EdgeVerve, Sistemas de extração, Hyland, Hyperscience, Infrrd e Parascript
  • Aspirantes: Ikarus, Rossum, Shipmnts (Alex), Amazon (Textract), Docsumo, Docparser, Aidock

A categoria em que eu gostaria de colocar esse problema seria a aprendizagem multimodal , porque as modalidades textual e de imagem contribuem bastante nesse problema. Embora os tokens de OCR desempenhem um papel vital na classificação de atributo-valor, sua posição na página, espaçamento e distâncias entre caracteres são características muito importantes na detecção de limites de tabela, linha e coluna. O problema fica ainda mais interessante quando as linhas quebram nas páginas ou algumas colunas carregam valores não vazios.

Enquanto o mundo acadêmico e as conferências usam o termo Processamento Inteligente de Documentos , em geral, para extrair campos singulares e dados tabulares. O primeiro é mais conhecido pela classificação de atributo-valor e o segundo é famoso por extração de tabela ou extração de estrutura repetida, na literatura de pesquisa.

Em nossa incursão no processamento desses documentos semiestruturados ao longo dos 3 anos, sinto que alcançar precisão e escalabilidade é uma jornada longa e árdua. As soluções que oferecem escalabilidade / abordagem 'sem modelo' têm anotado corpus de documentos comerciais semiestruturados na ordem de dezenas de milhares, se não milhões. Embora essa abordagem seja uma solução escalável, é tão boa quanto os documentos em que foi treinada. Se seus documentos provêm do setor de logística ou seguro, conhecidos por seus layouts complexos e precisam ser super precisos devido aos procedimentos de conformidade, uma solução 'baseada em modelo' seria a panacéia para seus males. É garantido para dar mais precisão.

Se você precisar de links para pesquisas existentes, mencione nos comentários abaixo e será um prazer compartilhá-los.

Além disso, eu recomendaria o uso do pdfparser 1 sobre o pdf2text ou pdfminer porque o primeiro fornece informações sobre o nível de caractere em arquivos digitais com desempenho significativamente melhor.

Ficaria feliz em incorporar qualquer feedback, pois esta é minha primeira resposta aqui.

SIDDHARTH SAHANI
fonte
Se você está procurando um repositório de código aberto, github.com/invoice-x/invoice2data pode ser um bom ponto de partida
SIDDHARTH SAHANI
3

Aqui está uma tentativa de usar o OpenCV, a ideia é:

  1. Obter imagem binária. Carregamos a imagem, aumentamos usando imutils.resizepara ajudar a obter melhores resultados de OCR (consulte o Tesseract para melhorar a qualidade ), convertemos em escala de cinza e, em seguida, o limite do Otsu para obter uma imagem binária (1 canal).

  2. Remova as linhas de grade da tabela. Criamos um núcleo horizontal e vertical e, em seguida, executamos operações morfológicas para combinar contornos de texto adjacentes em um único contorno. A idéia é extrair uma linha de ROI como uma peça para o OCR.

  3. Extrair ROIs de linha. Nós encontrar contornos em seguida, classificar a partir de cima para baixo usando imutils.contours.sort_contours. Isso garante que iteremos por cada linha na ordem correta. A partir daqui, iteramos pelos contornos, extraímos a linha ROI usando o faturamento Numpy, o OCR usando o Pytesseract e depois analisamos os dados.


Aqui está a visualização de cada etapa:

Imagem de entrada

insira a descrição da imagem aqui

Imagem binária

insira a descrição da imagem aqui

Morph fechar

insira a descrição da imagem aqui

Visualização da iteração através de cada linha

insira a descrição da imagem aqui

ROIs de linhas extraídas

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

Resultado dos dados da fatura de saída:

{'line': '0', 'tariff': '85444290', 'quantity': '3', 'amount': '258.93'}
{'line': '1', 'tariff': '85444290', 'quantity': '4', 'amount': '548.32'}
{'line': '2', 'tariff': '76109090', 'quantity': '5', 'amount': '412.30'}

Infelizmente, obtenho resultados variados ao experimentar a segunda e a terceira imagem. Este método não produz grandes resultados nas outras imagens, pois o layout das faturas é todo diferente. No entanto, essa abordagem mostra que é possível usar técnicas tradicionais de processamento de imagem para extrair as informações da fatura, supondo que você tenha um layout fixo de fatura.

Código

import cv2
import numpy as np
import pytesseract
from imutils import contours
import imutils

pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"

# Load image, enlarge, convert to grayscale, Otsu's threshold
image = cv2.imread('1.png')
image = imutils.resize(image, width=1000)
height, width = image.shape[:2]
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# Remove horizontal lines
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50,1))
detect_horizontal = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
cnts = cv2.findContours(detect_horizontal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    cv2.drawContours(thresh, [c], -1, 0, -1)

# Remove vertical lines
vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,50))
detect_vertical = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
cnts = cv2.findContours(detect_vertical, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    cv2.drawContours(thresh, [c], -1, 0, -1)

# Morph close to combine adjacent contours into a single contour
invoice_data = []
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (85,5))
close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=3)

# Find contours, sort from top-to-bottom
# Iterate through contours, extract row ROI, OCR, and parse data
cnts = cv2.findContours(close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
(cnts, _) = contours.sort_contours(cnts, method="top-to-bottom")

row = 0
for c in cnts:
    x,y,w,h = cv2.boundingRect(c)
    ROI = image[y:y+h, 0:width]
    ROI = cv2.GaussianBlur(ROI, (3,3), 0)
    data = pytesseract.image_to_string(ROI, lang='eng', config='--psm 6')
    parsed = [word.lower() for word in data.split()] 
    if 'tariff' in parsed or 'number' in parsed:
        row_data = {}
        row_data['line'] = str(row)
        row_data['tariff'] = parsed[-1]
        row_data['quantity'] = parsed[2]
        row_data['amount'] = str(max(parsed[10], parsed[11]))
        row += 1

        print(row_data)
        invoice_data.append(row_data)

        # Visualize row extraction
        '''
        mask = np.zeros(image.shape, dtype=np.uint8)
        cv2.rectangle(mask, (0, y), (width, y + h), (255,255,255), -1)
        display_row = cv2.bitwise_and(image, mask)

        cv2.imshow('ROI', ROI)
        cv2.imshow('display_row', display_row)
        cv2.waitKey(1000)
        '''
print(invoice_data)
cv2.imshow('thresh', thresh)
cv2.imshow('close', close)
cv2.waitKey()
nathancy
fonte
3
Obrigado @nathancy! Embora essa resposta não seja uma solução genérica para todas as minhas faturas lançadas, ainda acho que é a mais próxima que vi alguém chegar - e isso é apenas o OpenCV. Muito legal e seu exemplo de código me ensinou muito! Mais uma vez, obrigado por dedicar seu tempo postando isso.
oliverbj 14/02