Extraiga información de texto de archivos PDF con diferentes diseños: aprendizaje automático

8

Necesito ayuda con un proyecto de ML que estoy tratando de crear actualmente.

Recibo muchas facturas de muchos proveedores diferentes, todos en su propio diseño único. Necesito extraer 3 elementos clave de las facturas. Estos 3 elementos están ubicados en una tabla / partidas individuales para todas las facturas.

Los 3 elementos son:

  • 1 : número de tarifa (dígito)
  • 2 : Cantidad (siempre un dígito)
  • 3 : Importe total de la línea (valor monetario)

Consulte la siguiente captura de pantalla, donde he marcado estos campos en una factura de muestra.

ingrese la descripción de la imagen aquí

Comencé este proyecto con un enfoque de plantilla, basado en expresiones regulares . Sin embargo, esto no era escalable en absoluto y terminé con toneladas de reglas diferentes.

Espero que el aprendizaje automático pueda ayudarme aquí, ¿o tal vez una solución híbrida?

El común denominador

En todas mis facturas, a pesar de los diferentes diseños, cada línea de pedido siempre constará de un número de tarifa . Este número de tarifa siempre tiene 8 dígitos y siempre está formateado de una de las formas siguientes:

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

(Donde "x" es un dígito de 0 a 9).

Además , como puede ver en la factura, hay un precio unitario y un monto total por línea. La cantidad que necesitaré siempre es la más alta para cada línea.

La salida

Para cada factura como la anterior, necesito la salida para cada línea. Esto podría ser, por ejemplo, algo como esto:

{
    "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"
}

A dónde ir desde aquí?

No estoy seguro de qué es lo que estoy buscando hacer en el aprendizaje automático y, de ser así, en qué categoría. ¿Es visión por computadora? PNL? Reconocimiento de entidad con nombre?

Mi pensamiento inicial fue:

  1. Convierta la factura en texto. (Todas las facturas están en archivos PDF con texto, por lo que puedo usar algo como pdftotextobtener los valores textuales exactos)
  2. Crear personalizada entidades nombradas para quantity, tariffyamount
  3. Exportar las entidades encontradas.

Sin embargo, siento que me podría estar perdiendo algo.

¿Alguien puede ayudarme en la dirección correcta?

Editar:

Vea a continuación algunos ejemplos más de cómo puede verse una sección de la tabla de facturas:

Ejemplo de factura # 2 ingrese la descripción de la imagen aquí

Ejemplo de factura # 3 ingrese la descripción de la imagen aquí

Edición 2:

Consulte a continuación las tres imágenes de muestra, sin los bordes / cuadros delimitadores:

Imagen 1: Muestra 1 sin bbox

Imagen 2: Muestra 2 sin bbox

Imagen 3: Muestra 3 sin bbox

oliverbj
fuente
¿Puede mostrar algunos ejemplos más de los PDF de entrada para ver cuánta variación hay realmente? (= qué tan flexible será la solución)
sjaustirni
@sjaustirni ¡Acabo de agregar dos más! Creo que la mayor variación entre las facturas del proveedor es cómo está el diseño de la tabla (y posteriormente las líneas de pedido y cómo se formatea el texto específico)
oliverbj
¡Perfecto! Dados estos ejemplos, probablemente convertiría el pdf a texto e intentaría emparejar los valores allí con la etiqueta anterior (por ejemplo, Tariff No.:or $) o la columna a la que pertenece (aquí puede ayudarlo a guardar la información espacial de las letras, si alguna herramienta de OCR hace eso). Creo que no necesita entrar en el aprendizaje automático con este problema (aparte del OCR prefabricado), ni PNL (no es lenguaje natural). Sin embargo, sin ver qué tan bien funcionan estas herramientas con sus datos, solo podemos especular cuál es el siguiente paso y qué es necesario: D
sjaustirni
@sjaustirni, ¿no terminaría eso en lo mismo que ya estoy haciendo, que no es escalable? (Enfoque basado en plantillas / expresiones regulares).
oliverbj
¿No puede extraer la tabla misma del pdf a una estructura de datos y luego procesar las columnas? Puede ser que pueda usar tabula-py para hacer esto, y luego obtener la cantidad y el total directamente, y con algunas
expresiones

Respuestas:

4

Estoy trabajando en un problema similar en la industria de la logística y confía en mí cuando digo que estas tablas de documentos vienen en innumerables diseños. Numerosas compañías que han resuelto algo y están mejorando en este problema se mencionan como en

  • Líderes: ABBYY, AntWorks, Kofax y WorkFusion
  • Principales contendientes: Automatización en cualquier lugar, Celaton, Datamatics, EdgeVerve, Extract Systems, Hyland, Hyperscience, Infrrd y Parascript
  • Aspirantes: Ikarus, Rossum, Shipmnts (Alex), Amazon (Textract), Docsumo, Docparser, Aidock

La categoría a la que me gustaría someter este problema sería el aprendizaje multimodal , porque tanto las modalidades textuales como las de imagen contribuyen mucho en este problema. Aunque los tokens OCR juegan un papel vital en la clasificación de valor de atributo, su posición en la página, el espaciado y las distancias entre caracteres son características muy importantes en la detección de límites de tablas, filas y columnas. El problema se vuelve aún más interesante cuando las filas se dividen en páginas, o algunas columnas llevan valores no vacíos.

Mientras que el mundo académico y las conferencias usan el término Procesamiento inteligente de documentos , en general para extraer campos singulares y datos tabulares. El primero es más conocido por la clasificación de valor de atributo y el segundo es famoso por la extracción de tablas o la extracción de estructura repetida, en la literatura de investigación.

En nuestra incursión en el procesamiento de estos documentos semiestructurados durante los 3 años, siento que lograr precisión y escalabilidad es un viaje largo y arduo. Las soluciones que ofrecen el enfoque de escalabilidad / 'plantilla libre' tienen un corpus anotado de documentos comerciales semiestructurados del orden de decenas de miles, si no millones. Aunque este enfoque es una solución escalable, es tan bueno como los documentos en los que se ha capacitado. Si sus documentos provienen del sector de logística o seguros, que son conocidos por sus diseños complejos, y necesitan ser muy precisos debido a los procedimientos de cumplimiento, una solución 'basada en plantillas' sería la panacea para sus enfermedades. Se garantiza que dará más precisión.

Si necesita enlaces a investigaciones existentes, mencione en los comentarios a continuación y me complacerá compartirlos.

Además, recomendaría usar pdfparser 1 sobre pdf2text o pdfminer porque el primero proporciona información de nivel de caracteres en archivos digitales con un rendimiento significativamente mejor.

Estaría encantado de incorporar cualquier comentario, ya que esta es mi primera respuesta aquí.

SIDDHARTH SAHANI
fuente
Si está buscando un repositorio de código abierto, github.com/invoice-x/invoice2data podría ser un buen punto de partida
SIDDHARTH SAHANI
3

Aquí hay un intento de usar OpenCV, la idea es:

  1. Obtener imagen binaria. Cargamos la imagen, la ampliamos usando imutils.resizepara ayudar a obtener mejores resultados de OCR (ver Tesseract mejorar la calidad ), convertir a escala de grises, luego el umbral de Otsu para obtener una imagen binaria (1 canal).

  2. Eliminar las líneas de la cuadrícula de la tabla. Creamos núcleos horizontales y verticales, luego realizamos operaciones morfológicas para combinar contornos de texto adyacentes en un solo contorno. La idea es extraer una fila de ROI como una pieza para OCR.

  3. Extraer el ROI de la fila. Nos encontramos con contornos continuación, ordenar, de arriba a abajo usando imutils.contours.sort_contours. Esto garantiza que recorramos cada fila en el orden correcto. Desde aquí, iteramos a través de los contornos, extraemos el ROI de la fila usando Numpy slicing, OCR usando Pytesseract , y luego analizamos los datos.


Aquí está la visualización de cada paso:

Imagen de entrada

ingrese la descripción de la imagen aquí

Imagen binaria

ingrese la descripción de la imagen aquí

Morph cerca

ingrese la descripción de la imagen aquí

Visualización de iteración a través de cada fila

ingrese la descripción de la imagen aquí

ROI de fila extraída

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

Resultado de los datos de la factura de salida:

{'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'}

Desafortunadamente, obtengo resultados mixtos al probar la segunda y tercera imagen. Este método no produce excelentes resultados en las otras imágenes, ya que el diseño de las facturas es diferente. Sin embargo, este enfoque muestra que es posible utilizar técnicas de procesamiento de imágenes tradicionales para extraer la información de la factura con el supuesto de que tiene un diseño de factura fijo.

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
fuente
3
Gracias @nathancy! Si bien esta respuesta no es una solución genérica para todas mis facturas publicadas, sigo pensando que es lo más cercano que he visto a alguien venir, y esto solo está usando OpenCV. ¡Muy genial y tu código de ejemplo me enseñó mucho! Nuevamente, gracias por tomarse su tiempo para publicar esto.
oliverbj