importar os, sys, io, json, tiempo, matemáticas, re, subproceso, advertencias de colecciones importar contador, defaultdict advertencias.filterwarnings(“ignorar”) os.environ.setdefault(“USE_TORCH”, “1”) def _pip(*pkgs): subprocess.run([sys.executable, “-m”, “pip”, “install”, “-q”, *pkgs]check=False) intente: importar doctr excepto ImportError: print(“>> Instalando python-doctr (esto toma ~1-2 minutos en Colab)…”) _pip(“python-doctr[viz]”) intente: importar reportlab excepto ImportError: _pip(“reportlab”) importar numpy como np importar antorcha importar matplotlib importar matplotlib.pyplot como plt desde matplotlib importar font_manager desde matplotlib.patches importar Rectángulo, Polígono como MplPolygon desde PIL importar Imagen, ImageDraw, ImageFont importar doctr desde doctr.io importar DocumentFile desde doctr.models importar ( ocr_predictor, kie_predictor, deteccion_predictor, reconocimiento_predictor,) DISPOSITIVO = “cuda” si torch.cuda.is_available() else “cpu” print(“=” * 78) print(f”docTR: {doctr.__version__}”) print(f”torch: {torch.__version__}”) print(f”dispositivo: {DISPOSITIVO}” + (f” ({torch.cuda.get_device_name(0)})” if DEVICE == “cuda” else “”)) print(f”python: {sys.version.split()[0]}”) print(“=” * 78) print(“NOTA: si la importación anterior falló, reinicie el tiempo de ejecución ” “(Tiempo de ejecución > Reiniciar sesión) y vuelva a ejecutar esta celda.\n”) CFG = dict( RUN_BENCHMARK = True, RUN_SECOND_PASS = True, RUN_ROTATION = True, RUN_LAYOUT = True, RUN_KIE = True, RUN_SYNTHESIS = True, RUN_PDF_EXPORT = True, ) WORK = “/content/doctr_demo” if os.path.isdir(“/content”) else “./doctr_demo” os.makedirs(WORK, exist_ok=True) print(f”working dir: {WORK}\n”) _FONT = font_manager.findfont(font_manager.FontProperties(family=”DejaVu Sans”)) _FONT_B = font_manager.findfont( font_manager.FontProperties(family=”DejaVu Sans”, peso=”bold”)) A4 = (1240, 1754) FACTURA_LINES = [
( 80, 70, “NORTHWIND TRADING CO.”, 38, True ),
( 80, 122, “42 Harbour Road, Bristol BS1 5TY”, 22, False),
( 80, 152, “VAT GB 884 5521 09”, 22, False),
(820, 70, “INVOICE”, 44, True ),
(820, 132, “Invoice No: INV-2024-00817”, 22, False),
(820, 162, “Date: 14/03/2024”, 22, False),
(820, 192, “Due Date: 13/04/2024”, 22, False),
( 80, 260, “BILL TO”, 24, True ),
( 80, 296, “Aurora Robotics Ltd”, 24, False),
( 80, 328, “Unit 7 Fenway Business Park”, 22, False),
( 80, 358, “Cambridge CB4 0WS”, 22, False),
( 80, 388, “Contact: [email protected]”,22, Falso), ( 80, 470, “DESCRIPCIÓN”, 24, Verdadero ), (640, 470, “CANTIDAD”, 24, Verdadero ), (780, 470, “PRECIO UNIDAD”, 24, Verdadero ), (1010,470, “CANTIDAD”, 24, Verdadero ), ( 80, 520, “Placa de servocontrolador Rev C”, 22, Falso), (640, 520, “12”, 22, Falso), (780, 520, “84.50”, 22, Falso), (1010,520, “1014.00”, 22, Falso), ( 80, 560, “Caja de cambios de transmisión armónica 50:1”, 22, Falso), (640, 560, “4”, 22, False), (780, 560, “312.75”, 22, False), (1010,560, “1251.00”, 22, False), ( 80, 600, “Cable codificador blindado 2m”, 22, False), (640, 600, “20”, 22, False), (780, 600, “11.40”, 22, False), (1010,600, “228.00”, 22, False), ( 80, 640, “Servicio de calibración en sitio”, 22, False), (640, 640, “1”, 22, False), (780, 640, “450.00”, 22, False), (1010,640, “450.00”, 22, Falso), (780, 720, “Subtotal”, 22, Falso), (1010,720, “2943.00”, 22, Falso), (780, 756, “IVA 20%”, 22, Falso), (1010,756, “588.60”, 22, Falso), (780, 796, “TOTAL VENCIDO”, 26, Verdadero), (1010,796, “3531.60”, 26, Verdadero), (80, 900, “TÉRMINOS DE PAGO”, 24, Verdadero), (80, 936, “30 días netos. Los pagos atrasados acumulan intereses al 2% mensual.”, 20, Falso), ( 80, 968, “Banco: Lloyds Sort Code: 30-96-26 Cuenta: 41775302”, 20, Falso), ( 80,1010, “Referencia: INV-2024-00817”, 20, Falso), ]PAGE2_LINES = [
( 80, 70, “APPENDIX A – DELIVERY SCHEDULE”, 34, True ),
( 80, 140, “All shipments leave the Bristol warehouse before 16:00 GMT.”, 22, False),
( 80, 176, “Tracking numbers are emailed on the day of dispatch.”, 22, False),
( 80, 240, “MILESTONE”, 24, True ),
(700, 240, “TARGET DATE”, 24, True ),
( 80, 288, “Purchase order acknowledged”, 22, False),
(700, 288, “18/03/2024”, 22, False),
( 80, 328, “Controller boards shipped”, 22, False),
(700, 328, “25/03/2024”, 22, False),
( 80, 368, “Gearboxes shipped”, 22, False),
(700, 368, “02/04/2024”, 22, False),
( 80, 408, “On-site calibration window”, 22, False),
(700, 408, “08/04/2024”, 22, False),
( 80, 480, “Questions? Call +44 117 496 0022 or email [email protected]”, 20, False), ]def render_page(lines, size=A4, bg=250): “””Dibuja una página de documento limpia a partir de una lista de (x, y, texto, tamaño, negrita).””” img = Image.new(“RGB”, size, (bg, bg, bg)) d = ImageDraw.Draw(img) for x, y, text, sz, negrita en líneas: fuente = ImageFont.truetype(_FONT_B si está en negrita, sino _FONT, sz) d.text((x, y), texto, relleno=(18, 18, 22), fuente=fuente) d.line([(80, 455), (1160, 455)]relleno=(60, 60, 60), ancho=2) d.line([(80, 505), (1160, 505)]relleno=(160, 160, 160), ancho=1) d.line([(760, 700), (1160, 700)]fill=(60, 60, 60), width=2) return img def scanify(img, angle=0.0, noise=6.0, jpeg_quality=72, blur_shadow=True): “””Degradar un renderizado limpio para que se comporte como una foto de teléfono/escaneo de superficie plana.””” if angle: img = img.rotate(angle, expand=True, resample=Image.BICUBIC, fillcolor=(250, 250, 250)) arr = np.asarray(img).astype(np.float32) si blur_shadow: h, w = arr.shape[:2]
gx = np.linspace(-1, 1, w)[None, :]
gy = np.linspace(-1, 1, h)[:, None]
sombra = 1.0 – 0.10 * (gx ** 2 + 0.6 * gy ** 2) arr *= sombra[…, None]
si ruido: arr += np.random.normal(0, ruido, arr.shape) arr = np.clip(arr, 0, 255).astype(np.uint8) out = Image.fromarray(arr) if jpeg_quality: buf = io.BytesIO() out.save(buf, format=”JPEG”, calidad=jpeg_quality) buf.seek(0) out = Image.open(buf).convert(“RGB”) devuelve clean1 = render_page(INVOICE_LINES) clean2 = render_page(PAGE2_LINES) page1_path = os.path.join(WORK, “invoice_p1.png”) page2_path = os.path.join(WORK, “invoice_p2.png”) rotated_path = os.path.join(WORK, “invoice_rotated.png”) pdf_path = os.path.join(WORK, “invoice.pdf”) scanify(clean1, ángulo=0.4).save(page1_path) scanify(clean2, ángulo=-0.3).save(page2_path) scanify(clean1, ángulo=13.0, ruido=8.0).save(rotated_path) clean1.save(pdf_path, save_all=Verdadero, append_images=[clean2]resolución=150) GT_WORDS_P1 = [w for _, _, t, _, _ in INVOICE_LINES for w in t.split()]
print(f”generado: {page1_path}, {page2_path}, {rotated_path}, {pdf_path}”) print(f”palabras reales en la página 1: {len(GT_WORDS_P1)}\n”) fig, ax = plt.subplots(1, 3, figsize=(15, 7)) for a, im, t in zip(ax, [Image.open(page1_path), Image.open(page2_path),
Image.open(rotated_path)],
[“page 1 (scanified)”, “page 2”, “rotated 13 deg”]): a.imshow(im); a.set_title(t, tamaño de fuente=10); a.axis(“apagado”) plt.tight_layout(); plt.show() imgs_doc = DocumentFile.from_images([page1_path, page2_path]) pdf_doc = DocumentFile.from_pdf(pdf_path) pdf_hi = DocumentFile.from_pdf(pdf_path, escala=3) rot_doc = DocumentFile.from_images(rotated_path) print(“from_images :”, [p.shape for p in imgs_doc]imgs_doc[0].dtype) print(“desde_pdf :”, [p.shape for p in pdf_doc]) imprimir(“desde_pdf x3:”, [p.shape for p in pdf_hi]) print(“”” Reglas generales para `escala`: * el texto del cuerpo debe tener >= ~10 px de alto para que el modelo de reconocimiento sea feliz * escala=2 (predeterminado) se adapta a escaneos de 150-300 ppp; aumente a 3-4 para texto denso de 8 puntos * también puede pasar matrices numerosas sin procesar directamente a cualquier predictor: predictor([np.asarray(pil_image)]) * DocumentFile.from_url(…) también existe, pero necesita la [html] extra “””) def build_ocr(det=”db_resnet50″, reco=”crnn_vgg16_bn”, **kw): “””Construya un predictor de OCR y muévalo a la GPU cuando haya uno.””” model = ocr_predictor(det_arch=det, reco_arch=reco, pretrained=True, **kw) if DEVICE == “cuda”: intente: model = model.cuda() excepto Excepción como e: print(f” (ubicación de cuda omitida: {e})”) return model def timeit(fn, *args, calentamiento=1, ejecuciones=3, **kw): “””Calentamiento (carga de peso / cudnn autotune / inicio diferido), luego cronometrar correctamente.””” for _ in range(warmup): fn(*args, **kw) if DEVICE == “cuda”: torch.cuda.synchronize() t0 = time.perf_counter() out = Ninguno para _ dentro del rango(ejecuciones): out = fn(*args, **kw) if DEVICE == “cuda”: torch.cuda.synchronize() devuelve, (time.perf_counter() – t0) / ejecuta predictor = build_ocr() resultado, dt = timeit(predictor, imgs_doc, run=2) print(f”\nbaseline de un extremo a otro: {dt:.2f}s para {len(imgs_doc)} páginas ” f”({dt/len(imgs_doc):.2f}s/página en {DEVICE})”) print(f”primeros 90 caracteres de la página 1: {result.pages[0].prestar()[:90]!r}”)
( 80, 70, “NORTHWIND TRADING CO.”, 38, True ),
( 80, 122, “42 Harbour Road, Bristol BS1 5TY”, 22, False),
( 80, 152, “VAT GB 884 5521 09”, 22, False),
(820, 70, “INVOICE”, 44, True ),
(820, 132, “Invoice No: INV-2024-00817”, 22, False),
(820, 162, “Date: 14/03/2024”, 22, False),
(820, 192, “Due Date: 13/04/2024”, 22, False),
( 80, 260, “BILL TO”, 24, True ),
( 80, 296, “Aurora Robotics Ltd”, 24, False),
( 80, 328, “Unit 7 Fenway Business Park”, 22, False),
( 80, 358, “Cambridge CB4 0WS”, 22, False),
( 80, 388, “Contact: [email protected]”,22, Falso), ( 80, 470, “DESCRIPCIÓN”, 24, Verdadero ), (640, 470, “CANTIDAD”, 24, Verdadero ), (780, 470, “PRECIO UNIDAD”, 24, Verdadero ), (1010,470, “CANTIDAD”, 24, Verdadero ), ( 80, 520, “Placa de servocontrolador Rev C”, 22, Falso), (640, 520, “12”, 22, Falso), (780, 520, “84.50”, 22, Falso), (1010,520, “1014.00”, 22, Falso), ( 80, 560, “Caja de cambios de transmisión armónica 50:1”, 22, Falso), (640, 560, “4”, 22, False), (780, 560, “312.75”, 22, False), (1010,560, “1251.00”, 22, False), ( 80, 600, “Cable codificador blindado 2m”, 22, False), (640, 600, “20”, 22, False), (780, 600, “11.40”, 22, False), (1010,600, “228.00”, 22, False), ( 80, 640, “Servicio de calibración en sitio”, 22, False), (640, 640, “1”, 22, False), (780, 640, “450.00”, 22, False), (1010,640, “450.00”, 22, Falso), (780, 720, “Subtotal”, 22, Falso), (1010,720, “2943.00”, 22, Falso), (780, 756, “IVA 20%”, 22, Falso), (1010,756, “588.60”, 22, Falso), (780, 796, “TOTAL VENCIDO”, 26, Verdadero), (1010,796, “3531.60”, 26, Verdadero), (80, 900, “TÉRMINOS DE PAGO”, 24, Verdadero), (80, 936, “30 días netos. Los pagos atrasados acumulan intereses al 2% mensual.”, 20, Falso), ( 80, 968, “Banco: Lloyds Sort Code: 30-96-26 Cuenta: 41775302”, 20, Falso), ( 80,1010, “Referencia: INV-2024-00817”, 20, Falso), ]PAGE2_LINES = [
( 80, 70, “APPENDIX A – DELIVERY SCHEDULE”, 34, True ),
( 80, 140, “All shipments leave the Bristol warehouse before 16:00 GMT.”, 22, False),
( 80, 176, “Tracking numbers are emailed on the day of dispatch.”, 22, False),
( 80, 240, “MILESTONE”, 24, True ),
(700, 240, “TARGET DATE”, 24, True ),
( 80, 288, “Purchase order acknowledged”, 22, False),
(700, 288, “18/03/2024”, 22, False),
( 80, 328, “Controller boards shipped”, 22, False),
(700, 328, “25/03/2024”, 22, False),
( 80, 368, “Gearboxes shipped”, 22, False),
(700, 368, “02/04/2024”, 22, False),
( 80, 408, “On-site calibration window”, 22, False),
(700, 408, “08/04/2024”, 22, False),
( 80, 480, “Questions? Call +44 117 496 0022 or email [email protected]”, 20, False), ]def render_page(lines, size=A4, bg=250): “””Dibuja una página de documento limpia a partir de una lista de (x, y, texto, tamaño, negrita).””” img = Image.new(“RGB”, size, (bg, bg, bg)) d = ImageDraw.Draw(img) for x, y, text, sz, negrita en líneas: fuente = ImageFont.truetype(_FONT_B si está en negrita, sino _FONT, sz) d.text((x, y), texto, relleno=(18, 18, 22), fuente=fuente) d.line([(80, 455), (1160, 455)]relleno=(60, 60, 60), ancho=2) d.line([(80, 505), (1160, 505)]relleno=(160, 160, 160), ancho=1) d.line([(760, 700), (1160, 700)]fill=(60, 60, 60), width=2) return img def scanify(img, angle=0.0, noise=6.0, jpeg_quality=72, blur_shadow=True): “””Degradar un renderizado limpio para que se comporte como una foto de teléfono/escaneo de superficie plana.””” if angle: img = img.rotate(angle, expand=True, resample=Image.BICUBIC, fillcolor=(250, 250, 250)) arr = np.asarray(img).astype(np.float32) si blur_shadow: h, w = arr.shape[:2]
gx = np.linspace(-1, 1, w)[None, :]
gy = np.linspace(-1, 1, h)[:, None]
sombra = 1.0 – 0.10 * (gx ** 2 + 0.6 * gy ** 2) arr *= sombra[…, None]
si ruido: arr += np.random.normal(0, ruido, arr.shape) arr = np.clip(arr, 0, 255).astype(np.uint8) out = Image.fromarray(arr) if jpeg_quality: buf = io.BytesIO() out.save(buf, format=”JPEG”, calidad=jpeg_quality) buf.seek(0) out = Image.open(buf).convert(“RGB”) devuelve clean1 = render_page(INVOICE_LINES) clean2 = render_page(PAGE2_LINES) page1_path = os.path.join(WORK, “invoice_p1.png”) page2_path = os.path.join(WORK, “invoice_p2.png”) rotated_path = os.path.join(WORK, “invoice_rotated.png”) pdf_path = os.path.join(WORK, “invoice.pdf”) scanify(clean1, ángulo=0.4).save(page1_path) scanify(clean2, ángulo=-0.3).save(page2_path) scanify(clean1, ángulo=13.0, ruido=8.0).save(rotated_path) clean1.save(pdf_path, save_all=Verdadero, append_images=[clean2]resolución=150) GT_WORDS_P1 = [w for _, _, t, _, _ in INVOICE_LINES for w in t.split()]
print(f”generado: {page1_path}, {page2_path}, {rotated_path}, {pdf_path}”) print(f”palabras reales en la página 1: {len(GT_WORDS_P1)}\n”) fig, ax = plt.subplots(1, 3, figsize=(15, 7)) for a, im, t in zip(ax, [Image.open(page1_path), Image.open(page2_path),
Image.open(rotated_path)],
[“page 1 (scanified)”, “page 2”, “rotated 13 deg”]): a.imshow(im); a.set_title(t, tamaño de fuente=10); a.axis(“apagado”) plt.tight_layout(); plt.show() imgs_doc = DocumentFile.from_images([page1_path, page2_path]) pdf_doc = DocumentFile.from_pdf(pdf_path) pdf_hi = DocumentFile.from_pdf(pdf_path, escala=3) rot_doc = DocumentFile.from_images(rotated_path) print(“from_images :”, [p.shape for p in imgs_doc]imgs_doc[0].dtype) print(“desde_pdf :”, [p.shape for p in pdf_doc]) imprimir(“desde_pdf x3:”, [p.shape for p in pdf_hi]) print(“”” Reglas generales para `escala`: * el texto del cuerpo debe tener >= ~10 px de alto para que el modelo de reconocimiento sea feliz * escala=2 (predeterminado) se adapta a escaneos de 150-300 ppp; aumente a 3-4 para texto denso de 8 puntos * también puede pasar matrices numerosas sin procesar directamente a cualquier predictor: predictor([np.asarray(pil_image)]) * DocumentFile.from_url(…) también existe, pero necesita la [html] extra “””) def build_ocr(det=”db_resnet50″, reco=”crnn_vgg16_bn”, **kw): “””Construya un predictor de OCR y muévalo a la GPU cuando haya uno.””” model = ocr_predictor(det_arch=det, reco_arch=reco, pretrained=True, **kw) if DEVICE == “cuda”: intente: model = model.cuda() excepto Excepción como e: print(f” (ubicación de cuda omitida: {e})”) return model def timeit(fn, *args, calentamiento=1, ejecuciones=3, **kw): “””Calentamiento (carga de peso / cudnn autotune / inicio diferido), luego cronometrar correctamente.””” for _ in range(warmup): fn(*args, **kw) if DEVICE == “cuda”: torch.cuda.synchronize() t0 = time.perf_counter() out = Ninguno para _ dentro del rango(ejecuciones): out = fn(*args, **kw) if DEVICE == “cuda”: torch.cuda.synchronize() devuelve, (time.perf_counter() – t0) / ejecuta predictor = build_ocr() resultado, dt = timeit(predictor, imgs_doc, run=2) print(f”\nbaseline de un extremo a otro: {dt:.2f}s para {len(imgs_doc)} páginas ” f”({dt/len(imgs_doc):.2f}s/página en {DEVICE})”) print(f”primeros 90 caracteres de la página 1: {result.pages[0].prestar()[:90]!r}”)